Skip to content
Open

dev #162

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5d3b44b
fix: label max len
superlevure Jun 18, 2026
5a25c15
feat: build dev branch too
superlevure Jun 18, 2026
2a38cc4
chore: add debug logging
superlevure Jun 18, 2026
4b92d62
fix: config
superlevure Jun 18, 2026
5a0a757
fix: lint
superlevure Jun 18, 2026
0fc6a92
fix: style
superlevure Jun 18, 2026
f9fcae8
chore: temporarily disable pipeline integration test
superlevure Jun 18, 2026
128e7a2
chore: temporarily disable pipeline integration test
superlevure Jun 18, 2026
7b3675d
chore: debug
superlevure Jun 18, 2026
a6d6073
chore: debug
superlevure Jun 18, 2026
feb0b55
chore: debug
superlevure Jun 18, 2026
20b67a6
chore: remove debug logging
superlevure Jun 18, 2026
a0f0d0a
fix: remove default provider from load_config in job config
superlevure Jun 18, 2026
16e094e
chore: update model_config syntax in AppSettings
superlevure Jun 18, 2026
3e6c247
chore: less logging
superlevure Jun 19, 2026
5538bfc
fix: logging
superlevure Jun 19, 2026
81fceb0
chore: test with hardcoded system prompt
superlevure Jun 19, 2026
75772a9
dev: push image no matter what
superlevure Jun 19, 2026
5a7641d
chore: update agent sdk
superlevure Jun 19, 2026
7bcd793
fix: uv
superlevure Jun 19, 2026
82328e4
chore: revert system prompt
superlevure Jun 19, 2026
8c348d9
chore: logging
superlevure Jun 19, 2026
eef5603
chore: logging
superlevure Jun 19, 2026
e734131
chore: logging
superlevure Jun 19, 2026
79454e3
chore: logging
superlevure Jun 19, 2026
e7fd3e7
fix: error handling
superlevure Jun 19, 2026
7f45bf9
chore: logging
superlevure Jun 19, 2026
0715ce4
chore: logging
superlevure Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 49 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ name: CI

on:
push:
branches: [main]
tags: ["[0-9]+.[0-9]+.[0-9]+"]
branches: [main, dev]
tags: ['[0-9]+.[0-9]+.[0-9]+']
pull_request:
branches: [main]

Expand All @@ -19,7 +19,7 @@ jobs:

- uses: astral-sh/setup-uv@v6
with:
python-version: "3.13"
python-version: '3.13'

- run: uv sync

Expand Down Expand Up @@ -47,7 +47,7 @@ jobs:

- uses: astral-sh/setup-uv@v6
with:
python-version: "3.13"
python-version: '3.13'

- run: uv sync

Expand All @@ -70,7 +70,7 @@ jobs:

- uses: astral-sh/setup-uv@v6
with:
python-version: "3.13"
python-version: '3.13'

- name: Install with ${{ matrix.extras }} extra only
run: uv sync --no-default-groups --group test --extra ${{ matrix.extras }}
Expand All @@ -89,7 +89,7 @@ jobs:

- uses: astral-sh/setup-uv@v6
with:
python-version: "3.13"
python-version: '3.13'

- run: uv sync

Expand Down Expand Up @@ -174,7 +174,7 @@ jobs:

- uses: astral-sh/setup-uv@v6
with:
python-version: "3.13"
python-version: '3.13'

- run: uv sync

Expand Down Expand Up @@ -208,7 +208,7 @@ jobs:

- uses: astral-sh/setup-uv@v6
with:
python-version: "3.13"
python-version: '3.13'

- run: uv sync

Expand Down Expand Up @@ -264,7 +264,7 @@ jobs:

- uses: astral-sh/setup-uv@v6
with:
python-version: "3.13"
python-version: '3.13'

- run: uv sync

Expand All @@ -279,7 +279,13 @@ jobs:
run: uv run pytest tests/integration/ -m integration_webhook_server -v

cleanup:
needs: [integration-test-local, integration-test-pipeline, integration-test-webhook-server, integration-test-kubernetes]
needs:
[
integration-test-local,
integration-test-pipeline,
integration-test-webhook-server,
integration-test-kubernetes,
]
if: always() && github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
Expand All @@ -304,14 +310,14 @@ jobs:

ci:
needs:
- check
- unit-test
- unit-test-provider-isolation
- integration-test-local
# - check
# - unit-test
# - unit-test-provider-isolation
- build-image
- integration-test-pipeline
- integration-test-webhook-server
- integration-test-kubernetes
# - integration-test-local
# - integration-test-pipeline
# - integration-test-webhook-server
# - integration-test-kubernetes
if: always()
runs-on: ubuntu-latest
steps:
Expand All @@ -320,11 +326,17 @@ jobs:

promote:
needs: [ci]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev')
runs-on: ubuntu-latest
strategy:
matrix:
image: [nominal-code, nominal-code-anthropic, nominal-code-openai, nominal-code-google]
image:
[
nominal-code,
nominal-code-anthropic,
nominal-code-openai,
nominal-code-google,
]
permissions:
packages: write

Expand All @@ -336,10 +348,19 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Retag ${{ matrix.image }}:latest
- name: Determine target tag
id: tag
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "tag=latest" >> "$GITHUB_OUTPUT"
else
echo "tag=dev" >> "$GITHUB_OUTPUT"
fi

- name: Retag ${{ matrix.image }}:${{ steps.tag.outputs.tag }}
run: |
docker buildx imagetools create \
--tag ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}:latest \
--tag ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}:${{ steps.tag.outputs.tag }} \
ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}:${{ github.sha }}

release:
Expand All @@ -348,7 +369,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
image: [nominal-code, nominal-code-anthropic, nominal-code-openai, nominal-code-google]
image:
[
nominal-code,
nominal-code-anthropic,
nominal-code-openai,
nominal-code-google,
]
permissions:
packages: write

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ workspaces/

# Local K8s secrets (user-specific, never commit)
deploy/k8s/secret.yaml

monitor_promote_job.py
122 changes: 70 additions & 52 deletions app/nominal_code/agent/cli/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
AssistantMessage,
ClaudeAgentOptions,
Message,
ProcessError,
ResultMessage,
SystemMessage,
UserMessage,
Expand Down Expand Up @@ -117,67 +118,84 @@ async def run_cli_agent(
cli_path=cli_path,
resume=conversation_id,
system_prompt=system_prompt or None,
stderr=lambda msg: logger.info("[agent stderr] %s", msg),
)

result: AgentResult | None = None
returned_conversation_id: str | None = None

async for message in query(prompt=prompt, options=options):
_log_message(message)

if (
isinstance(message, SystemMessage)
and message.subtype == CONVERSATION_ID_INIT_SUBTYPE
):
returned_conversation_id = message.data.get("session_id", None)

if isinstance(message, ResultMessage):
output: str = message.result or ""
returned_conversation_id = message.session_id or returned_conversation_id

cli_cost: CostSummary | None = None
usage_dict: dict[str, Any] | None = (
message.usage if isinstance(message.usage, dict) else None
)

if message.total_cost_usd is not None or usage_dict:
cli_cost = CostSummary(
total_input_tokens=(usage_dict or {}).get("input_tokens", 0),
total_output_tokens=(usage_dict or {}).get("output_tokens", 0),
total_cache_creation_tokens=(usage_dict or {}).get(
"cache_creation_input_tokens",
0,
),
total_cache_read_tokens=(usage_dict or {}).get(
"cache_read_input_tokens",
0,
),
total_cost_usd=message.total_cost_usd,
provider=ProviderName.ANTHROPIC,
model=options.model or "",
try:
async for message in query(prompt=prompt, options=options):
_log_message(message)

if (
isinstance(message, SystemMessage)
and message.subtype == CONVERSATION_ID_INIT_SUBTYPE
):
returned_conversation_id = message.data.get("session_id", None)

if isinstance(message, ResultMessage):
output: str = message.result or ""
returned_conversation_id = (
message.session_id or returned_conversation_id
)

if message.is_error:
result = AgentResult(
output="",
num_turns=message.num_turns,
duration_ms=message.duration_ms,
conversation_id=returned_conversation_id,
cost=cli_cost,
error=InvocationError(
type=ErrorType.RUNTIME_ERROR,
message=output,
),
)
else:
result = AgentResult(
output=output,
num_turns=message.num_turns,
duration_ms=message.duration_ms,
conversation_id=returned_conversation_id,
cost=cli_cost,
cli_cost: CostSummary | None = None
usage_dict: dict[str, Any] | None = (
message.usage if isinstance(message.usage, dict) else None
)

if message.total_cost_usd is not None or usage_dict:
cli_cost = CostSummary(
total_input_tokens=(usage_dict or {}).get("input_tokens", 0),
total_output_tokens=(usage_dict or {}).get("output_tokens", 0),
total_cache_creation_tokens=(usage_dict or {}).get(
"cache_creation_input_tokens",
0,
),
total_cache_read_tokens=(usage_dict or {}).get(
"cache_read_input_tokens",
0,
),
total_cost_usd=message.total_cost_usd,
provider=ProviderName.ANTHROPIC,
model=options.model or "",
)

if message.is_error:
result = AgentResult(
output="",
num_turns=message.num_turns,
duration_ms=message.duration_ms,
conversation_id=returned_conversation_id,
cost=cli_cost,
error=InvocationError(
type=ErrorType.RUNTIME_ERROR,
message=output,
),
)
else:
result = AgentResult(
output=output,
num_turns=message.num_turns,
duration_ms=message.duration_ms,
conversation_id=returned_conversation_id,
cost=cli_cost,
)

except ProcessError as exc:
logger.error("Agent process error: %s", str(exc))
result = AgentResult(
output="",
num_turns=0,
duration_ms=0,
conversation_id=returned_conversation_id,
error=InvocationError(
type=ErrorType.RUNTIME_ERROR,
message=str(exc),
),
)

if result is not None:
return result

Expand Down
12 changes: 12 additions & 0 deletions app/nominal_code/agent/invoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ def prepare_conversation(
pr_number=event.pr_number,
namespace=namespace,
)
logger.info(
"Loaded conversation ID for %s#%d: %s",
event.repo_full_name,
event.pr_number,
conversation_id or "<none>",
)
elif isinstance(agent_config, ApiAgentConfig) and conversation_store is not None:
prior_messages = conversation_store.get_messages(
platform=event.platform,
Expand Down Expand Up @@ -196,3 +202,9 @@ def save_conversation(
value=list(result.messages),
namespace=namespace,
)
logger.info(
"Saved %d messages for %s#%d.",
len(result.messages),
event.repo_full_name,
event.pr_number,
)
3 changes: 1 addition & 2 deletions app/nominal_code/commands/webhook/jobs/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from nominal_code.config.settings import DEFAULT_REDIS_KEY_TTL_SECONDS, RedisConfig
from nominal_code.conversation.base import ConversationStore, build_conversation_store
from nominal_code.llm.cost import format_cost_summary
from nominal_code.models import ProviderName
from nominal_code.platforms import build_platform
from nominal_code.platforms.base import Platform, PlatformName
from nominal_code.review.reviewer import ReviewScope
Expand Down Expand Up @@ -190,7 +189,7 @@ def _build_job_config() -> Config:
ValueError: If ``AGENT_PROVIDER`` is not a recognised provider.
"""

return load_config(default_provider=ProviderName.GOOGLE)
return load_config()


def _build_redis_config() -> RedisConfig:
Expand Down
4 changes: 3 additions & 1 deletion app/nominal_code/commands/webhook/jobs/runner/kubernetes.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def _build_job_spec(
dict[str, Any]: The complete Job resource spec.
"""

repo_slug: str = _slugify(job.event.repo_full_name)
repo_slug: str = _slugify(job.event.repo_full_name)[:63]

labels: dict[str, str] = {
"app.kubernetes.io/name": "nominal-code",
Expand Down Expand Up @@ -229,6 +229,7 @@ def _build_job_spec(
}

container["volumeMounts"] = [
{"name": "config", "mountPath": "/etc/nominal-code", "readOnly": True},
{"name": "workspace", "mountPath": "/workspace"},
{"name": "tmp", "mountPath": "/tmp"},
{"name": "home", "mountPath": "/home/nominal"},
Expand Down Expand Up @@ -269,6 +270,7 @@ def _build_job_spec(

pod_spec["automountServiceAccountToken"] = False
pod_spec["volumes"] = [
{"name": "config", "configMap": {"name": "nominal-code-config"}},
{"name": "workspace", "emptyDir": {}},
{"name": "tmp", "emptyDir": {}},
{"name": "home", "emptyDir": {}},
Expand Down
Loading
Loading