fix(edge): 하이브리드 단계 책임을 복원한다

Planner와 Reviewer의 축약 계약을 system 경계에 두고 Worker가 실제 Review 증거를 직접 남기도록 해 terminal 요약에 의한 인계 누락을 막는다.
This commit is contained in:
toki 2026-08-15 09:24:28 +09:00
parent c051529c05
commit 4f1faa56e3
16 changed files with 447 additions and 132 deletions

View file

@ -94,19 +94,24 @@ response model semantics.
An unmarked `light` preset with `workspace_tools` uses only the tool schemas
admitted by the caller request. Edge never receives or configures a raw caller
workspace path. Before issuing caller writes, Edge validates the selector's
default lightweight PLAN and pending REVIEW seed and retains the PLAN `P1..Pn`
inventory request-locally. Selector instructions require every PLAN field to be
English ASCII, and Edge rejects a non-ASCII PLAN before issuing caller writes.
lightweight PLAN and pending REVIEW seed and retains the PLAN `P1..Pn` inventory
request-locally. Its Edge-owned system instruction applies a compact Plan contract:
analyze the immutable task first, preserve explicit requirements and acceptance,
close 2-6 executable steps and deterministic verification, and put the exact REVIEW
handoff duty in the final step. Every PLAN field remains English ASCII and Edge
rejects a non-ASCII PLAN before issuing caller writes.
Work receives only the absolute PLAN path resolved from the caller's existing working-directory system context, followed by the fixed English sentence
`Read it and complete the task.` and sees only ordinary caller tools. It must read
PLAN and perform and verify caller-workspace work. After a successful PLAN read and
a non-empty terminal worker report, Edge renders and validates the completed REVIEW
handoff and projects its single caller write; Work does not own that protocol. Only
PLAN, perform and verify caller-workspace work, and replace the exact REVIEW artifact
with completed item statuses, actual changes, verification evidence, and deviations.
Edge validates and projects that Worker-authored REVIEW write; it never synthesizes
Review evidence from terminal prose. Only
exact PLAN/REVIEW accesses contribute artifact state; every other caller-workspace
tool call passes through unchanged. Review
must read both artifacts and successfully inspect an ordinary caller result before
it can return a non-empty caller-visible terminal. It never rewrites REVIEW; repair
and re-verification remain in the same Review binding. Cleanup targets only the
tool call passes through unchanged. Review alone receives an Edge-owned compact
code-review system instruction. It must read both artifacts, inspect the actual
caller result, and rerun applicable verification before returning a non-empty
caller-visible terminal. It never rewrites REVIEW or creates a Result document;
root-cause repair and re-verification remain in the same Review binding. Cleanup targets only the
request's `.iop/job/<request_id>` directory.
After Edge issues the request-local artifact paths, it adds one bounded Edge-owned,
@ -116,14 +121,14 @@ the provider must emit exactly one admitted prepare call for
`.iop/job/<request_id>` and must not write PLAN or REVIEW. After the successful
prepare receipt, the resumed selector receives `pair-write` and must emit exactly
one atomic artifact-pair call containing the complete PLAN and REVIEW content with
the accepted default PLAN grammar and deterministic pending REVIEW grammar. Edge
the accepted bounded PLAN grammar and matching pending REVIEW inventory. Edge
expands that decision into two independently receipted admitted caller writes. A parent-creating write binding receives
`pair-write` immediately on its initial selector turn. In Chat Completions this is
the final leading `system` message; in native Messages it is the final top-level
`system` text block. The instruction is not caller input, does not alter the retained
caller request snapshot, and adds no public request/config field or raw workspace
path. Work, Review, cleanup, and marked `single_request` provider bodies do not
receive it.
path. Work, cleanup, and marked `single_request` provider bodies do not receive it;
Review receives only its separate compact code-review system instruction.
Continuation issue correlation hashes only the emitted tool-use identity, name, and
arguments/input. SDK-added text, thinking, and provider metadata decorations are not

View file

@ -267,7 +267,7 @@ Provider-pool raw passthrough route는 위 목록을 provider request allowlist
Chat Completions의 실행 경로는 caller가 보낸 `model`의 route/provider capability로 결정한다.
- provider-pool model group route(`models[]`)는 candidate를 선택한 뒤 selected provider가 OpenAI-compatible 호출 방식을 지원하면 provider HTTP status/header/body를 Node가 열어 기존 Edge-Node tunnel로 relay하고, Edge가 caller에게 쓴다. 요청 body는 라우팅에 필요한 envelope만 읽고 `model` alias를 selected provider의 served target으로 rewrite하는 것을 기본으로 하며, provider가 지원하는 OpenAI-compatible field와 provider extension field를 보존한다.
- Provider-pool Chat과 unmarked caller-workspace의 selector/Work/Review 요청은 selected protocol profile이 선언한 출력 상한 wire spelling을 사용한다. OpenAI Chat wire는 `max_completion_tokens`, Gemini Chat wire는 `max_tokens`로 정규화한다. 형식이 bounded인 selector PLAN/REVIEW seed는 caller SDK의 큰 기본 completion limit과 분리해 최대 4096 output tokens만 허용한다. Selector provider에는 prepare 단계의 canonical `prepare(path)` 또는 pair-write 단계의 단일 `iop_write_artifact_pair(plan_content,review_content)` schema만 보여준다. Selector instruction은 모든 PLAN field를 English ASCII로 작성하도록 요구하고 Edge는 non-ASCII PLAN을 caller write 전에 거부한다. Edge는 검증된 atomic pair를 PLAN/REVIEW 두 write로 확장하고 admitted caller tool의 실제 schema(예: `bash(command)`)로 각각 encode해 caller에게 투영한다. 따라서 selector에게 두 parallel tool call 생성을 요구하지 않는다. Work provider의 전체 task prompt는 caller의 기존 working-directory system context에서 해석한 absolute PLAN path와 고정 영문 `Read it and complete the task.`뿐이며 ordinary caller tools만 보인다. Work가 PLAN을 성공적으로 읽은 뒤 non-empty terminal report를 반환하면 Edge가 completed REVIEW template을 렌더·검증하고 단일 caller write로 투영한다. Work provider는 REVIEW handoff tool이나 protocol을 소유하지 않는다. 정확한 PLAN/REVIEW 접근만 artifact state로 기록하고 그 외 caller-workspace tool call은 경로 검사 없이 그대로 통과시킨다. Work/Review provider가 반환한 reserved-artifact operation은 실제 provider tool의 path alias(예: `filePath`)를 발행된 canonical path로 복원하고, 선택된 caller binding의 실제 tool 이름·argument mapping으로 정규화한 뒤 같은 codec으로 encode한다. 이 변환이 필요한 command-mode binding stage는 provider delta를 caller에게 선행 방출하지 않고 bounded stage를 수집·검증·매핑한 뒤 endpoint-native 응답으로 한 번 투영하며, 직접 투영 가능한 structured binding의 기존 progressive stream은 유지한다. Cleanup은 이미 caller-stage-only인 단일 call ID를 그대로 발행·해시하며 outer sequencer로 재투영하지 않는다. Caller continuation은 SDK가 assistant message에 추가·생략하는 content/reasoning/provider metadata가 아니라 발행된 tool-call ID·이름·arguments의 의미 해시로 issue를 결속한다. Unmarked 내부 stage가 function tools와 reasoning effort를 함께 요구하고 selected OpenAI profile의 Chat wire가 이를 지원하지 않으면, 같은 profile의 Responses operation이 두 요구를 보존할 때 내부 wire만 Responses로 전환한다. 이 내부 operation은 body와 tunnel metadata 모두 `stream=false`인 단일 buffered stage이며, provider가 거부하는 generic SDK sampling defaults인 `temperature``top_p`를 전달하지 않고 mapped effort를 generation control로 사용한다. Result decoder는 caller protocol이 아니라 selected operation을 우선하여 Responses JSON을 기존 hot-path 공통 stage shape로 복원하며, caller-visible Chat surface와 caller/agent identity로 분기하지 않는다.
- Provider-pool Chat과 unmarked caller-workspace의 selector/Work/Review 요청은 selected protocol profile이 선언한 출력 상한 wire spelling을 사용한다. OpenAI Chat wire는 `max_completion_tokens`, Gemini Chat wire는 `max_tokens`로 정규화한다. Bounded selector PLAN/REVIEW pair는 caller SDK의 큰 기본 completion limit과 분리해 최대 4096 output tokens만 허용한다. Selector provider에는 prepare 단계의 canonical `prepare(path)` 또는 pair-write 단계의 단일 `iop_write_artifact_pair(plan_content,review_content)` schema만 보여준다. Selector system instruction은 compact Plan 계약에 따라 immutable task를 먼저 분석하고 explicit requirement/constraint/acceptance를 보존한 2-6개 closed step, deterministic verification, exact REVIEW handoff duty를 작성하도록 하며 PLAN field를 English ASCII로 제한한다. Edge는 검증된 atomic pair를 admitted caller tool schema의 PLAN/REVIEW 두 write로 확장한다. Work provider의 전체 task prompt는 caller의 기존 working-directory system context에서 해석한 absolute PLAN path와 고정 영문 `Read it and complete the task.`뿐이며 ordinary caller tools만 보인다. Work는 PLAN을 읽고 구현·검증한 뒤 completed item status, actual changes, verification evidence, deviation을 담은 exact REVIEW write를 직접 반환한다. Edge는 이 Worker-authored handoff를 검증·투영하며 terminal prose로 REVIEW evidence를 합성하지 않는다. Review provider에만 별도 compact code-review system instruction을 주입한다. Review는 PLAN/REVIEW와 actual caller result를 읽고 applicable verification을 재실행하며, defect가 있으면 같은 binding에서 root cause를 확정해 repair/re-verify한 후에만 terminal을 반환한다. REVIEW를 다시 쓰거나 별도 Result 문서를 만들지 않는다. 정확한 PLAN/REVIEW 접근만 artifact state로 기록하고 그 외 caller-workspace tool call은 그대로 통과시킨다. Reserved-artifact operation은 provider tool의 path alias를 canonical path로 복원하고 caller binding의 tool/argument mapping으로 정규화한다. Command-mode binding stage는 provider delta를 선행 방출하지 않고 bounded stage를 수집·검증·매핑하며 structured binding의 progressive stream은 유지한다. Cleanup은 request-local job directory만 제거한다. Caller continuation은 SDK 장식이 아니라 발행된 tool-call ID·이름·arguments의 의미 해시로 issue를 결속한다. Function tools와 reasoning effort를 Chat wire가 함께 표현하지 못하면 같은 selected profile의 Responses operation으로 내부 wire만 전환한다. Result decoder는 selected operation을 우선하며 caller/agent identity로 분기하지 않는다.
- selected provider가 Ollama/native provider처럼 normalized execution을 요구하면 Edge는 normalized `RunRequest` path를 사용한다. 이 경로는 OpenAI-compatible 표면을 입력/출력 compatibility layer로 제공하되, backend 호출은 normalized adapter 계약을 따른다.
- `metadata`는 경로 선택자가 아니다. Edge는 route 결정 뒤 인증 principal, usage/observability 등 IOP가 아는 bounded metadata key만 발췌한다. 이 발췌 정보는 provider body를 바꾸는 selector가 아니며, passthrough 응답 body에 IOP marker/event/envelope를 섞지 않는다.
- Chat Completions 성공 응답의 top-level `model` echo가 provider-served model이면 caller가 요청한 IOP model alias로 정규화할 수 있다. reasoning/content/tool_calls 같은 provider payload field는 보존한다.

View file

@ -208,7 +208,7 @@ Edge가 OpenAI-compatible HTTP 요청을 받아 내부 `adapter + target` 실행
| managed projection auth | `credential_plane.enabled=true` uses the fresh Control Plane projection for inbound token auth and principal route discovery. Static principal/bearer fallback is disabled. |
| managed slot route | Public model id/alias resolves to one projected route, exact slot/profile/upstream model/resource selector, and immutable revisions/generation. Unknown, cross-principal, stale, revoked, or ambiguous bindings fail closed. |
| marked preset single-request admission | An authorized fixed single-request preset compiles one service-owned admission value at request start: requested public model, canonical plan/work/review bindings resolved through managed authorization, opaque workspace capability, and absolute resource caps. Later refresh cannot mutate the admitted shape. No private binding is echoed to the caller. Compiled only after every canonical reference is verified through its catalog binding for the authenticated principal; missing, duplicate, unauthorized, dynamically selected, or option-inconsistent inputs are rejected without fallback. |
| unmarked caller-workspace light route | An unmarked `light` preset binds only admitted caller `workspace_tools`; it never accepts a raw caller path. Edge derives a phase-specific selector instruction from the locked artifact frontier. A non-parent-creating write binding first receives `prepare-only` for exactly one admitted `.iop/job/<request_id>` prepare call with PLAN/REVIEW writes prohibited; its successful receipt resumes the same selector with `pair-write` for the accepted artifact grammar. A parent-creating binding receives `pair-write` immediately. The selector provider sees only canonical `prepare(path)` or one atomic `iop_write_artifact_pair(plan_content,review_content)` schema. The selector must write every PLAN field in English ASCII; Edge rejects non-ASCII PLAN content. Edge validates and expands the pair into two independently receipted writes encoded in the admitted caller tool schema such as `bash(command)`; the selector is not required to produce parallel tool calls. Continuation issue identity covers semantic tool-call ID/name/arguments rather than SDK-decorated assistant content, reasoning, or provider metadata, while request prefix and result IDs stay independently validated. The selector instruction is the final leading Chat `system` message or final Anthropic top-level `system` text block and does not mutate the retained caller snapshot or add a public field. Work receives exactly the absolute PLAN path resolved from the caller's existing working-directory system context plus `Read it and complete the task.` and only ordinary caller tools. After a successful PLAN read and a non-empty terminal worker report, Edge renders and validates the completed `P1..Pn` REVIEW handoff and projects its single caller write; Work does not manage that protocol. Exact PLAN/REVIEW access updates artifact state, while every other caller-workspace tool call passes through unchanged. Selector/Work/Review Chat bodies normalize `max_tokens`/`max_completion_tokens` to the selected profile wire; target-native values win when both aliases exist. The bounded selector PLAN/REVIEW seed is capped at 4096 output tokens independently of a caller SDK's larger completion default. When tools plus reasoning effort are not representable on the selected OpenAI Chat wire, the internal stage alone uses that profile's lossless buffered Responses operation with body/tunnel `stream=false`, omits incompatible generic SDK `temperature`/`top_p` defaults, and restores the common hot-path stage shape by selected operation rather than caller protocol. A selector upstream HTTP 400 becomes one non-retryable caller HTTP 400 `invalid_request_error`, while provider details remain private. Review reads both artifacts, successfully inspects an ordinary caller result, repairs/re-verifies in the same binding when needed, and owns the non-empty final output without rewriting REVIEW. Cleanup removes only the request `.iop/job/<request_id>` directory. Marked `single_request` presets bypass this route and retain the operator-owned Node workspace. |
| unmarked caller-workspace light route | An unmarked `light` preset binds only admitted caller `workspace_tools`; it never accepts a raw caller path. Edge derives `prepare-only` or atomic `pair-write` from the locked artifact frontier. The selector receives a compact Plan system contract: analyze the immutable task, preserve explicit requirements/constraints/acceptance, author 2-6 closed steps plus deterministic verification, and put the exact REVIEW handoff duty in the final step. PLAN remains English ASCII and pending REVIEW carries the same P1..Pn inventory. Work receives only the absolute PLAN path plus `Read it and complete the task.` and ordinary caller tools; no Plan/Review skill system prompt is added to Work. Work implements and verifies, then directly replaces REVIEW with completed statuses, actual changes, verification evidence, and deviations. Edge validates that Worker-authored write and never synthesizes it from terminal prose. Review alone receives the compact code-review system contract. It rereads PLAN/REVIEW, inspects the actual result, reruns applicable verification, repairs and reverifies in the same binding when needed, and owns the non-empty final output without rewriting REVIEW or creating a Result document. Exact artifact accesses update request-local state, ordinary caller tool calls pass through, and cleanup removes only `.iop/job/<request_id>`. Provider operation/effort/tool normalization remains selected-profile driven and caller identity independent. Marked `single_request` presets bypass this route and retain the operator-owned Node workspace. |
| marked single-request provider normalization | Plan/Work/Review derive caller-neutral effort/tool/structured-output requirements and let the selected protocol profile choose Chat Completions or Responses. Effort exact misses fall only to the nearest declared lower grade (`max` → `xhigh` when `max` is absent). Explicit resource selectors keep exact provider-ID verification; a `default` selector leaves provider choice to the pool while model group, profile, upstream model, credential slot/revision, and tunnel path remain frozen. Both Chat and Responses results are converted into the private common Chat-shaped stage codec before Plan/Work/Review validation. Chat conversion discards only bounded standard/provider bookkeeping (`service_tier`, `system_fingerprint`, provider `timings`, choice `logprobs`, message `annotations`, null `refusal`) and rejects a non-null refusal or unknown/duplicate fields. |
| marked single-request internal templates | The admission also freezes the operator-configured effective Plan/Review Markdown templates. They are internal artifact shapes only: the Plan stage first performs a bounded read/list-only workspace inspection and requires one successful result in the same request-local conversation, then obtains a strict one-line `goal` plus bounded one-line `steps`/`verification` arrays. Edge owns the bullet formatting and renders `plan.md`; no separate analysis artifact is created. The Review template shapes the private `review.md` artifact. Callers cannot supply, name, or select a template, and template paths, contents, and digests never appear in a response, error, log, or metric label. The caller-visible Messages request/response schema is unchanged and the final text stays the model's `decision.output`. |
| marked single-request ingress | One validated and authorized Messages POST enters the separate service coordinator capability before legacy provider/caller continuation and increments `iop_anthropic_single_request_ingress_total` once. Non-streaming returns one buffered final-only message. Streaming keeps one envelope across the coordinator lifetime, exposes only fixed plan/work/review/repair text blocks plus `event: ping`, and commits one final text/error terminal. Internal reasoning/tool wire never becomes caller `tool_use`; success is acknowledged only after the complete terminal write succeeds. |
@ -371,6 +371,8 @@ sequenceDiagram
## 변경 기록
- 2026-08-15: Corrected the unmarked caller-workspace hybrid responsibility contract. Planner and Reviewer now receive separate compact system contracts, Worker still receives only the absolute PLAN path plus the fixed sentence, Worker directly authors REVIEW evidence, and Review retains inspection/repair/re-verification without a Result artifact.
- 2026-08-15: Normalized Chat output-token aliases by selected protocol profile across provider-pool and unmarked caller-workspace selector/Work/Review calls. OpenAI Chat uses `max_completion_tokens`, Gemini Chat uses `max_tokens`, target-native values win, and unmarked internal stages use the selected OpenAI profile's Responses operation when tools plus effort cannot be represented by Chat. The reasoning Responses conversion omits incompatible generic SDK `temperature`/`top_p` defaults. Selector upstream HTTP 400 now terminates once as caller `invalid_request_error` instead of retryable 502. Command-mode workspace execution also carries the admitted containment guard in the actual outgoing command.
- 2026-08-14: Split the unmarked selector instruction into frontier-derived `prepare-only` and `pair-write` operations. Non-parent-creating bindings prepare the request job directory first and receive the exact artifact-pair grammar only after the successful receipt; parent-creating bindings receive the pair operation immediately.
- 2026-08-15: Replaced probabilistic parallel PLAN/REVIEW selector calls with one canonical atomic artifact-pair call that Edge deterministically expands into the same two admitted caller writes.

View file

@ -0,0 +1,75 @@
<!-- task=hybrid_pipeline_contract plan=0 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> Implementing agent: complete every implementation-owned section, run the Plan verification, and leave this active file in place. Review agent owns verdict and finalization.
## Overview
date=2026-08-15
task=hybrid_pipeline_contract, plan=0, tag=REFACTOR
## Implementation Item Completion
| Item | Status |
|---|---|
| REFACTOR-1 Restore Planner Contract | completed |
| REFACTOR-2 Make Reviewer Contract System-Owned | completed |
| REFACTOR-3 Restore Worker-Owned Review Handoff | completed |
| REFACTOR-4 Synchronize Contract And Run Qualification | in progress: local evidence complete, live qualification pending reviewed deployment |
## Implementation Checklist
- [x] Add the compact Agent-Ops Plan contract to the Planner system instruction while retaining atomic Plan/Review pair output and bounded grammar.
- [x] Inject the compact Agent-Ops Review/Repair contract as Reviewer-only system guidance across normalized, Chat, and Messages stage bodies without changing Worker input.
- [x] Require Worker-authored Review evidence as the local-stage handoff and remove Edge synthesis from terminal prose.
- [ ] Update focused tests, contracts, and current specs; run package, full Go, and live Gemini hybrid verification.
- [x] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
## Deviations from Plan
- Added `hot_path_review.go` and three existing cross-path fixture files to the exact write boundary because the `issueTools` signature and direct handoff semantics required their mechanical updates.
- `agent-spec/runtime/edge-node-execution.md` was not changed: its matching text describes the separate marked `single_request` Node-workspace path, while this change is limited to the unmarked caller-workspace light route.
- Live Gemini qualification remains after reviewed dev deployment as planned.
## Key Design Decisions
- Planner keeps the existing selector system-message injection and atomic pair tool; only its authored contract changed.
- Reviewer guidance is a separate system value on normalized input, a leading Chat system message, or Anthropic top-level system. Dynamic task/artifact/correlation data remains user-stage input.
- Worker prompt remains byte-for-byte `absolute PLAN path + fixed sentence` and receives no compact skill system prompt.
- A validated Worker Review tool call now supplies the local commit correlation. The receipt transitions directly to Review; terminal prose can no longer manufacture Review evidence.
## Reviewer Checkpoints
- Planner/Reviewer skill contracts are system-owned; Worker prompt remains exact and minimal.
- Worker writes Review evidence directly; Edge does not infer it from terminal prose.
- Reviewer reads artifacts and actual result, verifies, repairs if necessary, and reverifies in the same stage.
- Existing stage routing, paths, cleanup, and provider normalization remain intact.
## Verification Results
### Targeted Edge tests
Command: `go test ./apps/edge/internal/openai -count=1`
Result: PASS.
Output: `ok iop/apps/edge/internal/openai 8.719s`
### Full Go regression
Command: `go test ./... -count=1`
Result: PASS.
All Go packages passed, including `apps/edge/internal/openai`, `apps/edge/internal/service`, Node workspace/transport, config, and `packages/go/singlerequesttemplate`.
### Gemini hybrid qualification
Pending reviewed deployment and isolated live execution. No credential, provider endpoint, or raw model output has been written to this artifact.
## Review-Only Checklist
- [ ] Run applicable verification and inspect actual changed files.
- [ ] Append PASS/WARN/FAIL with findings, root cause, and selected fix when needed.
- [ ] Finalize the Agent-Ops task state according to the code-review skill.

View file

@ -0,0 +1,196 @@
<!-- task=hybrid_pipeline_contract plan=0 tag=REFACTOR -->
# Lightweight Hybrid Plan/Review Contract
## For the Implementing Agent
Follow this plan as written. Run every listed verification command and fill the implementation-owned sections in `CODE_REVIEW-cloud-G07.md` with actual changes and command output. Keep both active files in place and report ready for review. If blocked, record the exact blocker, attempted command/output, and resume condition only in the review evidence; do not archive task files or create another control-plane state.
## Background
The caller-workspace light pipeline already has selector, local worker, review, repair, and cleanup stages. Its current prompts and handoff ownership diverge from the intended compact Agent-Ops Plan/Review workflow: Plan is forced into a deterministic seed, Reviewer guidance is carried as ordinary stage text, and Edge can synthesize Review evidence from a worker terminal summary. This task preserves the state machine and corrects only those internal contracts before rerunning the Gemini hybrid benchmark.
## Analysis
### Files Read
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/finalize-task-routing/SKILL.md`
- `agent-ops/skills/common/plan/templates/review-stub-template.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-contract/index.md`
- `agent-contract/outer/anthropic-compatible-api.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-spec/index.md`
- `agent-spec/runtime/edge-node-execution.md`
- `agent-spec/input/openai-compatible-surface.md`
- `apps/edge/internal/openai/provider_model_rewrite.go`
- `apps/edge/internal/openai/hot_path_stage_input.go`
- `apps/edge/internal/openai/hot_path_dispatch.go`
- `apps/edge/internal/openai/hot_path_light.go`
- `apps/edge/internal/openai/hot_path_review.go`
- `apps/edge/internal/openai/artifact_pair.go`
- `packages/go/singlerequesttemplate/template.go`
- `apps/edge/internal/openai/provider_model_rewrite_test.go`
- `apps/edge/internal/openai/hot_path_direct_test.go`
- `apps/edge/internal/openai/hot_path_light_test.go`
- `apps/edge/internal/openai/hot_path_anthropic_gate_test.go`
- `apps/edge/internal/openai/hot_path_chat_gate_test.go`
- `apps/edge/internal/openai/hot_path_observation_test.go`
### SDD Criteria
Not applicable. This is a non-Milestone correction to an existing runtime contract.
### Verification Context
No external handoff was supplied. Repository source, existing hot-path tests, outer contracts, and current Gemini hybrid qualification requirements are the evidence sources. Unit verification uses the Edge package and full Go suite; live verification uses the existing dev deployment and the same Gemini hybrid benchmark path after implementation review. Live credentials and endpoints remain external runtime inputs and must not be written to tracked files or test output.
### Test Coverage Gaps
- Existing selector tests assert the deterministic two-step seed but do not assert compact Plan-skill analysis/acceptance/handoff instructions.
- Existing stage-input tests assert Worker prompt isolation but do not prove Reviewer guidance is a system message on both Chat and Messages wires.
- Existing light-flow fixtures exercise Edge-projected worker Review completion; they must instead prove the Worker-authored Review write is the local-stage terminal handoff.
- Existing review/repair tests cover artifact reads and repair transitions and should remain valid after system-prompt injection.
### Symbol References
- `projectWorkerCompletionReview` and `boundedWorkerCompletionSummary` are local to `hot_path_light.go`; removing them has no external call sites.
- `pendingLocalCommit` is used only by the local-handoff transition and can be populated from the Worker Review tool-call dispatch correlation instead of a synthesized terminal projection.
- `hotPathChatStageMessages`, `hotPathAnthropicStageBody`, and `hotPathStageRunInput` are the three internal provider shapes that must carry Reviewer-only system guidance without changing Worker input.
### Split Judgment
Keep one plan. Planner guidance, Worker handoff ownership, and Reviewer guidance form one cross-stage correctness invariant; splitting would allow an intermediate state in which artifacts or transitions no longer agree.
### Scope Rationale
Do not redesign execution presets, stage routing, provider normalization, artifact paths, cleanup, benchmark scoring, or the shared Plan/Review Markdown parser. Do not inject a Plan/Review skill prompt into Worker. Do not add a Result artifact. The current lightweight Plan/Review grammar remains bounded; only its authored content and stage ownership are corrected.
### Final Routing
- evaluation_mode: `first-pass`
- finalizer: `finalize-task-policy.sh`, mode `pair`
- build: closures all true; scores `scope=1,state=2,blast=1,evidence=1,verification=2`; `local-fit`, `local/G07`, `PLAN-local-G07.md`
- review: closures all true; scores `scope=1,state=2,blast=1,evidence=1,verification=2`; `official-review`, `cloud/G07`, `CODE_REVIEW-cloud-G07.md`
- large_indivisible_context: `false`
- matched_loop_risk_signatures: `temporal_state`, `boundary_contract`, `structured_interpretation` (count 3)
- review_rework_count: `0`
- evidence_integrity_failure: `false`
## Implementation Checklist
- [ ] Add the compact Agent-Ops Plan contract to the Planner system instruction while retaining atomic Plan/Review pair output and bounded grammar.
- [ ] Inject the compact Agent-Ops Review/Repair contract as Reviewer-only system guidance across normalized, Chat, and Messages stage bodies without changing Worker input.
- [ ] Require Worker-authored Review evidence as the local-stage handoff and remove Edge synthesis from terminal prose.
- [ ] Update focused tests, contracts, and current specs; run package, full Go, and live Gemini hybrid verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
### [REFACTOR-1] Restore Planner Contract
**Problem:** `apps/edge/internal/openai/provider_model_rewrite.go:36` tells the selector to replace a deterministic two-step seed, so explicit requirements and acceptance constraints can be lost before Work.
**Solution:** Keep the existing selector system-message injection and atomic pair tool. Replace the seed-oriented prose with a concise Plan-skill contract: analyze the immutable task before authoring, preserve all explicit requirements/constraints/acceptance, choose 2-6 closed executable steps, specify deterministic verification, and make the final step name the exact Review path and Worker evidence responsibility. Generate the pending Review status inventory for exactly the chosen P1..Pn ids.
**Modified Files and Checklist:**
- [ ] `apps/edge/internal/openai/provider_model_rewrite.go`: replace deterministic seed guidance with the compact Plan system contract.
- [ ] `apps/edge/internal/openai/provider_model_rewrite_test.go`: assert analysis/acceptance/handoff guidance and bounded atomic output.
- [ ] `apps/edge/internal/openai/hot_path_direct_test.go`: update the selector fixture's required compact-contract fragments.
**Test Strategy:** Update the existing selector instruction/canonical tool tests; no new harness.
**Verification:** `go test ./apps/edge/internal/openai -run 'TestHotPathSelector|TestProvider' -count=1` passes.
### [REFACTOR-2] Make Reviewer Contract System-Owned
**Problem:** `apps/edge/internal/openai/hot_path_stage_input.go:140` embeds review behavior in the ordinary stage prompt, while Chat and Messages provider bodies have no Reviewer-specific system contract.
**Solution:** Add one concise Reviewer system contract derived from the code-review skill: read Plan and worker Review, inspect actual output, rerun verification, establish findings/root cause/one fix, repair and reverify in the same stage, and return the final result without a separate Result document. Inject it only for Reviewer in normalized input, Chat system messages, and Anthropic top-level system. Keep dynamic task/artifact/correlation data in the ordinary prompt. Leave the local Worker prompt byte-for-byte unchanged.
**Modified Files and Checklist:**
- [ ] `apps/edge/internal/openai/hot_path_stage_input.go`: define Reviewer system guidance and reduce dynamic prompt to stage data/action.
- [ ] `apps/edge/internal/openai/hot_path_dispatch.go`: inject Reviewer-only system guidance into all internal provider request shapes.
- [ ] `apps/edge/internal/openai/hot_path_light_test.go`: prove system/user separation and unchanged Worker prompt for Chat and Messages.
**Test Strategy:** Extend existing stage-input and captured tunnel-body assertions.
**Verification:** `go test ./apps/edge/internal/openai -run 'TestHotPathStageInput|TestHotPathLight' -count=1` passes.
### [REFACTOR-3] Restore Worker-Owned Review Handoff
**Problem:** `apps/edge/internal/openai/hot_path_light.go:891` converts Worker terminal prose into completed Review fields, making Edge—not Worker—the evidence author and losing actual commands/deviations.
**Solution:** Remove terminal-summary Review projection. When the Worker emits the exact validated Review write after reading Plan, retain that provider dispatch correlation as the local commit; after the caller receipts the write, transition directly to Review. A terminal without a successful Plan read and Worker-authored Review remains a validation failure. Preserve ordinary caller tool pass-through and existing Review/Repair transitions.
**Modified Files and Checklist:**
- [ ] `apps/edge/internal/openai/hot_path_light.go`: commit direct Worker Review handoff and remove synthesized Review projection.
- [ ] `apps/edge/internal/openai/hot_path_review.go`: pass no Worker commit correlation through Reviewer tool dispatch.
- [ ] `apps/edge/internal/openai/hot_path_light_test.go`: update fixtures and add regression coverage for missing/malformed Worker evidence.
- [ ] `apps/edge/internal/openai/hot_path_anthropic_gate_test.go`: make the streaming fixture use a Worker-authored Review handoff.
- [ ] `apps/edge/internal/openai/hot_path_chat_gate_test.go`: keep mixed-provider coverage with a Worker-authored Review handoff.
- [ ] `apps/edge/internal/openai/hot_path_observation_test.go`: record the Review handoff as the local stage's final tool turn.
**Test Strategy:** Use existing scripted OpenAI and Anthropic fixtures with direct Worker Review writes; assert no local terminal summary is needed and malformed/missing handoff fails.
**Verification:** `go test ./apps/edge/internal/openai -run 'TestHotPathLight|TestWorker' -count=1` passes.
### [REFACTOR-4] Synchronize Contract And Run Qualification
**Problem:** Outer contracts/specs currently state that Edge renders completed Review from a Worker report, which would become stale after ownership is corrected.
**Solution:** Update only the affected current contract/spec statements. After code review and deployment, run the same Gemini hybrid benchmark in an isolated execution and capture request/start/completion timing, artifacts, actual output, review/repair behavior, and terminal result without secrets.
**Modified Files and Checklist:**
- [ ] `agent-contract/outer/anthropic-compatible-api.md`: record Planner/Reviewer system contracts and Worker-authored Review handoff.
- [ ] `agent-contract/outer/openai-compatible-api.md`: update the unmarked caller-workspace light route.
- [ ] `agent-spec/runtime/edge-node-execution.md`: synchronize the current hybrid stage responsibility summary if it contains the affected behavior.
- [ ] `agent-spec/input/openai-compatible-surface.md`: synchronize the caller-workspace hybrid behavior.
- [ ] `agent-task/hybrid_pipeline_contract/CODE_REVIEW-cloud-G07.md`: record actual implementation and verification evidence.
**Test Strategy:** Targeted and full Go regression, then existing live Gemini hybrid benchmark only; do not create another benchmark harness.
**Verification:** `go test ./... -count=1` passes, deployment health is confirmed, and one isolated Gemini hybrid execution produces worker-authored Review evidence plus a reviewer-verified final workspace result.
## Modified Files Summary
| File | Items |
|---|---|
| `apps/edge/internal/openai/provider_model_rewrite.go` | REFACTOR-1 |
| `apps/edge/internal/openai/provider_model_rewrite_test.go` | REFACTOR-1 |
| `apps/edge/internal/openai/hot_path_direct_test.go` | REFACTOR-1 |
| `apps/edge/internal/openai/hot_path_stage_input.go` | REFACTOR-2 |
| `apps/edge/internal/openai/hot_path_dispatch.go` | REFACTOR-2 |
| `apps/edge/internal/openai/hot_path_light.go` | REFACTOR-3 |
| `apps/edge/internal/openai/hot_path_review.go` | REFACTOR-3 |
| `apps/edge/internal/openai/hot_path_light_test.go` | REFACTOR-2, REFACTOR-3 |
| `apps/edge/internal/openai/hot_path_anthropic_gate_test.go` | REFACTOR-3 |
| `apps/edge/internal/openai/hot_path_chat_gate_test.go` | REFACTOR-3 |
| `apps/edge/internal/openai/hot_path_observation_test.go` | REFACTOR-3 |
| `agent-contract/outer/anthropic-compatible-api.md` | REFACTOR-4 |
| `agent-contract/outer/openai-compatible-api.md` | REFACTOR-4 |
| `agent-spec/runtime/edge-node-execution.md` | REFACTOR-4 |
| `agent-spec/input/openai-compatible-surface.md` | REFACTOR-4 |
| `agent-task/hybrid_pipeline_contract/CODE_REVIEW-cloud-G07.md` | REFACTOR-4 |
## Dependencies and Execution Order
REFACTOR-1 and REFACTOR-2 establish the prompt contracts. REFACTOR-3 changes the matching Worker handoff transition. REFACTOR-4 follows the implemented behavior and must not document unverified semantics.
## Final Verification
1. `gofmt -w apps/edge/internal/openai/provider_model_rewrite.go apps/edge/internal/openai/provider_model_rewrite_test.go apps/edge/internal/openai/hot_path_stage_input.go apps/edge/internal/openai/hot_path_dispatch.go apps/edge/internal/openai/hot_path_light.go apps/edge/internal/openai/hot_path_light_test.go`
2. `go test ./apps/edge/internal/openai -count=1`
3. `go test ./... -count=1`
4. Deploy the reviewed build to the existing dev environment using the project deployment skill.
5. Run one isolated Gemini hybrid benchmark through the existing path and verify the absolute Plan-only Worker prompt, Worker-authored Review evidence, actual workspace result, Reviewer inspection/repair if needed, one terminal, and cleanup.
After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.

View file

@ -152,8 +152,8 @@ func TestHotPathAnthropicDirectStreamPreservesEmptyToolInput(t *testing.T) {
func TestHotPathAnthropicLightStreamAggregatesStages(t *testing.T) {
fixture := newScriptedLightFixture(t, "anthropic", false)
fixture.service.responses[4] = func(string) string {
return scriptedLightCompletionWithUsage("anthropic", "local-visible", "local-reason", 5, 3)
fixture.service.responses[4] = func(requestID string) string {
return scriptedReviewWriteWithUsage("anthropic", requestID, 5, 3)
}
fixture.service.responses[5] = func(requestID string) string {
return scriptedReviewReadsAndInspectionWithUsage(requestID, 7, 4)

View file

@ -153,7 +153,7 @@ func TestHotPathChatMixedProviderStages(t *testing.T) {
{candidate: openAICandidate, body: func(requestID string) string { return scriptedArtifactPair("openai", requestID) }},
{candidate: openAICandidate, body: func(requestID string) string { return scriptedArtifactLocalRead("openai", requestID) }},
{candidate: openAICandidate, body: func(string) string { return scriptedLocalWorkTool("openai") }},
{candidate: openAICandidate, contentType: "text/event-stream", body: func(string) string { return hotPathChatMixedLocalSSE() }},
{candidate: openAICandidate, body: func(requestID string) string { return scriptedReviewWrite("openai", requestID) }},
{candidate: anthropicCandidate, body: func(requestID string) string { return scriptedReviewReadsAndInspectionWithUsage(requestID, 7, 3) }},
}

View file

@ -464,13 +464,16 @@ func scriptedSelectorDirective(providerBody []byte, operation string) (string, s
"Return exactly one iop_write_artifact_pair tool call",
"PLAN path: " + paths.PlanPath,
"REVIEW path: " + paths.ReviewPath,
"The default PLAN grammar accepts 2-6 consecutive steps and 1-3 verification bullets.",
"You are the Planner.",
"Analyze the immutable user task first.",
"Preserve every explicit requirement, constraint, deliverable, and acceptance condition.",
"# Plan\n\n## Goal\n<one non-empty line>\n\n## Steps\n- [P1] <non-empty one-line step>\n- [P2] <non-empty one-line step>",
"The pending REVIEW content must be exactly this deterministic seed:",
"The pending REVIEW must use exactly the Plan's P1..Pn inventory in order",
"# Review\n\n## Worker Item Status\n- P1: pending\n- P2: pending",
"## Worker Changes\nPending worker execution.",
"## Worker Verification\nPending worker verification.",
"## Deviations\nNone recorded.",
"Do not copy the explanatory optional-status text into REVIEW.",
}
for _, fragment := range required {
if !strings.Contains(instruction, fragment) {

View file

@ -1284,7 +1284,12 @@ func (s *Server) submitHotPathStage(ctx context.Context, r *http.Request, snapsh
metadata[principalMetaRef] = snapshot.PrincipalRef
}
applyTrustedManagedBindingMetadata(metadata, route)
estimate := estimateInputTokensBytes([]byte(prompt), metadata, snapshot.Tools, nil)
systemPrompt := snapshot.Input.systemPrompt(snapshot.Phase)
estimateInput := prompt
if systemPrompt != "" {
estimateInput = systemPrompt + "\n" + prompt
}
estimate := estimateInputTokensBytes([]byte(estimateInput), metadata, snapshot.Tools, nil)
contextClass := classifyContext(estimate, s.longContextThreshold())
runInput := hotPathStageRunInput(snapshot, prompt)
runReq := edgeservice.SubmitRunRequest{
@ -1712,6 +1717,9 @@ func hotPathStageMetaFromDispatch(stageID string, dispatch edgeservice.RunDispat
func hotPathStageRunInput(snapshot hotPathDispatchSnapshot, prompt string) map[string]any {
messages := hotPathChatStageMessages(snapshot, prompt)
input := map[string]any{"prompt": prompt, "messages": messages}
if systemPrompt := snapshot.Input.systemPrompt(snapshot.Phase); systemPrompt != "" {
input["system"] = systemPrompt
}
if tools := hotPathChatTools(hotPathStageProviderTools(snapshot)); len(tools) > 0 {
input["tools"] = tools
input["tool_choice"] = "auto"
@ -1750,6 +1758,9 @@ func hotPathAnthropicStageBody(snapshot hotPathDispatchSnapshot, prompt, target
body := map[string]any{
"model": target, "max_tokens": 4096, "messages": hotPathAnthropicStageMessages(snapshot, prompt), "stream": snapshot.Stream,
}
if systemPrompt := snapshot.Input.systemPrompt(snapshot.Phase); systemPrompt != "" {
body["system"] = systemPrompt
}
if tools := hotPathAnthropicTools(hotPathStageProviderTools(snapshot)); len(tools) > 0 {
body["tools"] = tools
body["tool_choice"] = map[string]any{"type": "auto"}
@ -1777,7 +1788,11 @@ func applyHotPathStageOptions(body map[string]any, options map[string]any, reser
}
func hotPathChatStageMessages(snapshot hotPathDispatchSnapshot, prompt string) []any {
messages := []any{map[string]any{"role": "user", "content": prompt}}
messages := make([]any, 0, len(snapshot.Transcript)*2+2)
if systemPrompt := snapshot.Input.systemPrompt(snapshot.Phase); systemPrompt != "" {
messages = append(messages, map[string]any{"role": "system", "content": systemPrompt})
}
messages = append(messages, map[string]any{"role": "user", "content": prompt})
for _, exchange := range snapshot.Transcript {
assistant := map[string]any{"role": "assistant", "content": exchange.Output.Content}
if exchange.Output.Reasoning != "" {

View file

@ -9,7 +9,6 @@ import (
"strings"
"sync"
"time"
"unicode/utf8"
edgeservice "iop/apps/edge/internal/service"
"iop/packages/go/config"
@ -449,6 +448,7 @@ func (s *hotPathLightStore) issueTools(
kind hotPathPendingKind,
outer *hotPathOuterTurn,
coordinator *logicalRequestCoordinator,
localCommit *hotPathStageCorrelation,
) (normalizedStageOutput, error) {
if s == nil || coordinator == nil {
return normalizedStageOutput{}, fmt.Errorf("light flow is unavailable")
@ -505,6 +505,14 @@ func (s *hotPathLightStore) issueTools(
if _, err := coordinator.awaitToolResults(requestID, ownerEdgeID, stageID, expected, issuedHash); err != nil {
return normalizedStageOutput{}, err
}
if kind == hotPathPendingLocalHandoff {
if localCommit == nil {
return normalizedStageOutput{}, fmt.Errorf("worker review handoff commit correlation is unavailable")
}
commit := *localCommit
commit.StageID = record.localStageID
record.pendingLocalCommit = &commit
}
record.pendingKind = kind
record.pending = pending
record.pendingHash = issuedHash
@ -888,82 +896,6 @@ func (s *hotPathLightStore) commitLocal(requestID, ownerEdgeID string, output no
return hotPathLightDisposition{RequestID: requestID, StageID: reviewStageID, Phase: record.phase}, nil
}
func (s *hotPathLightStore) projectWorkerCompletionReview(
requestID, ownerEdgeID string,
output normalizedStageOutput,
correlation hotPathStageCorrelation,
coordinator *logicalRequestCoordinator,
) (normalizedStageOutput, error) {
if s == nil {
return normalizedStageOutput{}, fmt.Errorf("light flow is unavailable")
}
if coordinator == nil {
return normalizedStageOutput{}, fmt.Errorf("logical request coordinator is unavailable")
}
s.mu.Lock()
defer s.mu.Unlock()
record := s.records[requestID]
if record == nil || record.ownerEdgeID != ownerEdgeID || record.phase != hotPathPhaseLocalActive || !record.running || len(output.ToolCalls) != 0 {
return normalizedStageOutput{}, fmt.Errorf("local flow state is unavailable")
}
if !record.localPlanRead {
return normalizedStageOutput{}, fmt.Errorf("worker completion requires a successful plan read")
}
summary := boundedWorkerCompletionSummary(output.Content)
if summary == "" {
return normalizedStageOutput{}, fmt.Errorf("worker completion summary is required")
}
statuses := make([]string, len(record.planItemIDs))
for index, itemID := range record.planItemIDs {
statuses[index] = fmt.Sprintf("- %s: completed", itemID)
}
review, err := singlerequesttemplate.RenderReview(
singlerequesttemplate.DefaultReviewTemplate,
singlerequesttemplate.ReviewFields{
ItemStatus: strings.Join(statuses, "\n"),
Changes: summary,
Verification: "Worker completion report: " + summary,
Deviations: "None reported by the worker.",
},
singlerequesttemplate.MaxTemplateBytes,
)
if err != nil || singlerequesttemplate.ValidateReviewHandoff(review, record.planItemIDs) != nil {
return normalizedStageOutput{}, fmt.Errorf("project worker completion review")
}
providerCallID, err := coordinator.newCallID()
if err != nil {
return normalizedStageOutput{}, err
}
correlation.StageID = record.localStageID
correlation.ResponseID = output.ResponseID
correlation.Terminal = output.TerminalReason
record.pendingLocalCommit = &correlation
projected := cloneNormalizedStageOutput(output)
projected.Content = ""
projected.Reasoning = ""
projected.ProgressivelyReleased = false
projected.CallerStageOnly = true
projected.ToolCalls = []normalizedToolCall{{
ID: providerCallID, ProviderCallID: providerCallID, Name: hotPathWorkerReviewToolName,
Path: newReservedPaths(requestID).ReviewPath,
Arguments: map[string]any{"content": string(review)},
}}
return projected, nil
}
func boundedWorkerCompletionSummary(content string) string {
const maximum = 2048
content = strings.TrimSpace(content)
if len(content) <= maximum {
return content
}
limit := maximum
for limit > 0 && !utf8.RuneStart(content[limit]) {
limit--
}
return strings.TrimSpace(content[:limit]) + "..."
}
func (s *hotPathLightStore) classifyLocalOutput(requestID, ownerEdgeID string, output normalizedStageOutput) (hotPathPendingKind, error) {
if s == nil {
return "", fmt.Errorf("light flow is unavailable")
@ -1132,15 +1064,6 @@ func (s *Server) runHotPathLightStage(w http.ResponseWriter, r *http.Request, di
if snapshot.Phase == hotPathPhaseLocalActive && len(output.ToolCalls) == 0 && outer.outputBudget().Exhausted {
return s.writeHotPathLightLengthTerminal(w, r, dispatch, protocol, stream, requestID, output)
}
if snapshot.Phase == hotPathPhaseLocalActive && len(output.ToolCalls) == 0 && !hotPathIsProviderLengthTerminal(output.TerminalReason) {
output, err = s.lightFlows.projectWorkerCompletionReview(
requestID, s.edgeIDValue(), output, correlation, s.requestCoordinator,
)
if err != nil {
return s.writeHotPathPrimaryError(w, r, dispatch, protocol, stream, requestID,
hotPathLightEndpointError(protocol, http.StatusBadRequest, err.Error()))
}
}
visible = mergeVisibleStageOutput(visible, output)
// The collector compatibility path remains the endpoint renderer until
// endpoint codecs consume released deltas directly. Feed the same
@ -1164,7 +1087,7 @@ func (s *Server) runHotPathLightStage(w http.ResponseWriter, r *http.Request, di
return s.writeHotPathPrimaryError(w, r, dispatch, protocol, stream, requestID,
hotPathLightEndpointError(protocol, http.StatusBadRequest, err.Error()))
}
mapped, err := s.lightFlows.issueTools(r.Context(), requestID, s.edgeIDValue(), output, visible, kind, outer, s.requestCoordinator)
mapped, err := s.lightFlows.issueTools(r.Context(), requestID, s.edgeIDValue(), output, visible, kind, outer, s.requestCoordinator, &correlation)
if err != nil {
return s.writeHotPathPrimaryError(w, r, dispatch, protocol, stream, requestID,
hotPathLightEndpointError(protocol, http.StatusBadRequest, err.Error()))

View file

@ -27,8 +27,8 @@ func TestHotPathLightLocalTransition(t *testing.T) {
t.Fatalf("final response: status=%d body=%s", final.Code, final.Body.String())
}
history, _ := json.Marshal(fixture.history)
if !strings.Contains(string(history), "local-complete-visible") {
t.Fatalf("local completion was not visible before review: history=%s", history)
if !strings.Contains(string(history), "worker-handoff-visible") {
t.Fatalf("worker-authored review handoff was not issued: history=%s", history)
}
fixture.assertCleanupCommitted(7)
})
@ -98,7 +98,7 @@ func TestWorkerProviderReceivesOnlyCallerTools(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if schemas["run_command"] == nil || schemas[hotPathWorkerReviewToolName] != nil || len(schemas) != 1 {
if schemas["run_command"] == nil || schemas["iop_write_worker_review"] != nil || len(schemas) != 1 {
t.Fatalf("worker provider tools=%+v", schemas)
}
}
@ -120,6 +120,22 @@ func TestHotPathLightRequiresPlanReadBeforeWorkerCompletion(t *testing.T) {
}
}
func TestHotPathLightRequiresWorkerAuthoredReviewAfterPlanRead(t *testing.T) {
for _, endpoint := range []string{"openai", "anthropic"} {
endpoint := endpoint
t.Run(endpoint, func(t *testing.T) {
fixture := newScriptedLightFixture(t, endpoint, false)
fixture.service.responses[4] = func(string) string {
return scriptedLightCompletion(endpoint, "terminal prose cannot replace review evidence")
}
driveScriptedLightToLocalAfterPlanRead(t, fixture)
localWork := fixture.request()
fixture.consumeToolResponse(localWork, []string{`{"ok":true}`})
assertLightFailureAfterCleanup(t, fixture, "completed review handoff")
})
}
}
func assertLightFailureAfterCleanup(t *testing.T, fixture *scriptedLightFixture, message string) {
t.Helper()
cleanup := fixture.request()
@ -411,7 +427,7 @@ func newScriptedLightFixture(t *testing.T, endpoint string, repair bool) *script
func(requestID string) string { return scriptedArtifactPair(endpoint, requestID) },
func(requestID string) string { return scriptedArtifactLocalRead(endpoint, requestID) },
func(string) string { return scriptedLocalWorkTool(endpoint) },
func(string) string { return scriptedLightCompletion(endpoint, "local-complete-visible") },
func(requestID string) string { return scriptedReviewWrite(endpoint, requestID) },
func(requestID string) string { return scriptedReviewReadsAndInspection(endpoint, requestID) },
}
if repair {
@ -555,9 +571,9 @@ func (f *scriptedLightFixture) assertCleanupCommitted(wantCalls int) {
selectorResponse = "msg-scripted-pair"
}
localResponse := "chatcmpl-light-complete"
localResponse := "chatcmpl-review-write"
if f.endpoint == "anthropic" {
localResponse = "msg-light-complete"
localResponse = "msg-review-write"
}
// Regression: local stage must carry selector correlation and must NOT
@ -662,6 +678,9 @@ func assertReviewCorrelationRegression(t *testing.T, req edgeservice.ProviderPoo
t.Fatalf("review Run.Input[\"prompt\"] is missing")
}
inputStr := fmt.Sprint(input)
if got := fmt.Sprint(req.Run.Input["system"]); got != hotPathReviewSystemPrompt {
t.Fatalf("review Run.Input system prompt mismatch: %q", got)
}
if !strings.Contains(prompt, "Committed selector stage success:") {
t.Fatalf("review Run.Prompt missing selector correlation: %s", prompt)
@ -690,13 +709,35 @@ func assertReviewCorrelationRegression(t *testing.T, req edgeservice.ProviderPoo
}
// Mandatory: decode and verify selected protocol tunnel prompt.
_, tunnelPrompt, err := decodeSelectedTunnelPrompt(req, selected)
prepared, tunnelPrompt, err := decodeSelectedTunnelPrompt(req, selected)
if err != nil {
t.Fatalf("review tunnel decode error: %v", err)
}
if tunnelPrompt != prompt {
t.Fatalf("review decoded tunnel prompt mismatch: got %q want %q", tunnelPrompt, prompt)
}
body, err := prepared.BuildBody("target-model")
if err != nil {
t.Fatal(err)
}
if selected.ProfileDriver == string(config.ProtocolDriverAnthropicMessages) {
var payload struct {
System string `json:"system"`
}
if err := json.Unmarshal(body, &payload); err != nil || payload.System != hotPathReviewSystemPrompt {
t.Fatalf("review Messages system prompt mismatch: system=%q err=%v body=%s", payload.System, err, body)
}
} else {
var payload struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
}
if err := json.Unmarshal(body, &payload); err != nil || len(payload.Messages) < 2 || payload.Messages[0].Role != "system" || payload.Messages[0].Content != hotPathReviewSystemPrompt {
t.Fatalf("review Chat system prompt mismatch: messages=%+v err=%v body=%s", payload.Messages, err, body)
}
}
if !strings.Contains(tunnelPrompt, "Committed selector stage success:") {
t.Fatalf("review tunnel body missing selector correlation: %s", tunnelPrompt)
}
@ -742,10 +783,12 @@ func decodeSelectedTunnelPrompt(req edgeservice.ProviderPoolDispatchRequest, sel
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
return prepared, "", fmt.Errorf("unmarshal anthropic payload: %w (body=%s)", err, string(bodyBytes))
}
if len(payload.Messages) == 0 || payload.Messages[0].Role != "user" {
return prepared, "", fmt.Errorf("anthropic body missing first user message: %s", string(bodyBytes))
for _, message := range payload.Messages {
if message.Role == "user" {
return prepared, extractMessageContentString(message.Content), nil
}
}
return prepared, extractMessageContentString(payload.Messages[0].Content), nil
return prepared, "", fmt.Errorf("anthropic body missing user message: %s", string(bodyBytes))
} else {
if prepared.Path != "/v1/chat/completions" || prepared.Operation != string(config.OperationChatCompletions) {
return prepared, "", fmt.Errorf("openai tunnel path/op mismatch: path=%q op=%q", prepared.Path, prepared.Operation)
@ -759,10 +802,12 @@ func decodeSelectedTunnelPrompt(req edgeservice.ProviderPoolDispatchRequest, sel
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
return prepared, "", fmt.Errorf("unmarshal openai payload: %w (body=%s)", err, string(bodyBytes))
}
if len(payload.Messages) == 0 || payload.Messages[0].Role != "user" {
return prepared, "", fmt.Errorf("openai body missing first user message: %s", string(bodyBytes))
for _, message := range payload.Messages {
if message.Role == "user" {
return prepared, extractMessageContentString(message.Content), nil
}
}
return prepared, extractMessageContentString(payload.Messages[0].Content), nil
return prepared, "", fmt.Errorf("openai body missing user message: %s", string(bodyBytes))
}
}

View file

@ -1497,7 +1497,7 @@ func hotPathPassTrace() []hotPathTracePoint {
{Event: hotPathEventClassDispatch},
{Event: hotPathEventClassStage, Stage: hotPathStageKindLocal, Attempt: hotPathAttemptFirst, Disposition: hotPathTerminalDispositionToolTurn},
{Event: hotPathEventClassStage, Stage: hotPathStageKindLocal, Attempt: hotPathAttemptRetry, Disposition: hotPathTerminalDispositionToolTurn},
{Event: hotPathEventClassStage, Stage: hotPathStageKindLocal, Attempt: hotPathAttemptRetry, Disposition: hotPathTerminalDispositionSuccess},
{Event: hotPathEventClassStage, Stage: hotPathStageKindLocal, Attempt: hotPathAttemptRetry, Disposition: hotPathTerminalDispositionToolTurn},
{Event: hotPathEventClassLight, Stage: hotPathStageKindReview, Attempt: hotPathAttemptFirst},
{Event: hotPathEventClassStage, Stage: hotPathStageKindReview, Attempt: hotPathAttemptFirst, Disposition: hotPathTerminalDispositionToolTurn},
{Event: hotPathEventClassStage, Stage: hotPathStageKindReview, Attempt: hotPathAttemptRetry, Disposition: hotPathTerminalDispositionSuccess},

View file

@ -35,7 +35,7 @@ func (s *Server) advanceHotPathReview(
}
return mapped, true, nil
}
mapped, err := s.lightFlows.issueTools(ctx, requestID, s.edgeIDValue(), output, visible, kind, outer, s.requestCoordinator)
mapped, err := s.lightFlows.issueTools(ctx, requestID, s.edgeIDValue(), output, visible, kind, outer, s.requestCoordinator, nil)
if err != nil {
return normalizedStageOutput{}, false, err
}

View file

@ -8,7 +8,13 @@ import (
"unicode"
)
const hotPathWorkerReviewToolName = "iop_write_worker_review"
const hotPathReviewSystemPrompt = `You are the Reviewer in a compact Plan -> Work -> Review -> Repair pipeline.
Read the exact issued Plan and worker-filled Review before judging the task. Treat Review as worker evidence, not proof.
Inspect the actual caller-workspace result and rerun the Plan's applicable verification with ordinary caller tools.
Check correctness, completeness, requirement coverage, verification trust, and unjustified deviations.
If a defect exists, establish its evidence and root cause, choose one concrete fix, repair it with ordinary caller tools, and reverify in this same Review stage.
Do not rewrite the reserved Plan or Review artifacts. Do not create a separate Result or final-review document.
Return a concise non-empty final result only after the result is verified; use no tool call in that terminal response.`
type hotPathArtifactPaths struct {
PlanPath string
@ -138,9 +144,9 @@ func (in hotPathStageInput) prompt(phase hotPathLightPhase) (string, error) {
case "review":
switch phase {
case hotPathPhaseReviewActive:
b.WriteString("Read both exact issued PLAN and REVIEW artifacts. Inspect at least one actual caller-workspace result with an ordinary caller tool. Do not rewrite REVIEW. If the work is correct, return a concise non-empty final result without a tool call. If repair is needed, use ordinary caller tools to repair and re-verify in this same review stage.")
b.WriteString("Review the task using the exact issued artifacts and caller workspace.")
case hotPathPhaseReviewRepair:
b.WriteString("Continue the same review-stage repair and verification with ordinary caller tools. Do not read or rewrite the reserved artifacts again. When finished, return a concise non-empty final result without a tool call.")
b.WriteString("Continue the same repair and verification from the retained Review-stage context.")
default:
return "", fmt.Errorf("review input cannot run in phase %q", phase)
}
@ -150,6 +156,18 @@ func (in hotPathStageInput) prompt(phase hotPathLightPhase) (string, error) {
return b.String(), nil
}
func (in hotPathStageInput) systemPrompt(phase hotPathLightPhase) string {
if in.Role != "review" {
return ""
}
switch phase {
case hotPathPhaseReviewActive, hotPathPhaseReviewRepair:
return hotPathReviewSystemPrompt
default:
return ""
}
}
func callerWorkingDirectory(task string) (string, error) {
for _, line := range strings.Split(task, "\n") {
line = strings.TrimSpace(line)

View file

@ -37,8 +37,14 @@ Operation: pair-write
Return exactly one iop_write_artifact_pair tool call containing both complete artifact contents and no other tool call. Do not mention or infer an absolute workspace path.
PLAN path: %s
REVIEW path: %s
The default PLAN grammar accepts 2-6 consecutive steps and 1-3 verification bullets. For this deterministic seed, use exactly the following two-step, one-verification form and replace only angle-bracketed text:
Write every PLAN field in English using ASCII characters only.
You are the Planner. Apply this compact Plan workflow before authoring the pair:
1. Analyze the immutable user task first. Do not create a separate analysis artifact.
2. Preserve every explicit requirement, constraint, deliverable, and acceptance condition. Do not invent extra scope.
3. Convert that analysis into 2-6 closed, executable steps. The Worker must be able to implement without rediscovering requirements or choosing among alternatives.
4. Write 1-3 deterministic verification bullets with observable pass conditions.
5. Make the final Plan step require the Worker to replace the exact REVIEW path above with completed item statuses, actual changes, actual verification evidence, and deviations. That Review write is the Work stage's final action.
Write every PLAN field in English using ASCII characters only and use exactly this grammar:
# Plan
## Goal
@ -47,15 +53,18 @@ Write every PLAN field in English using ASCII characters only.
## Steps
- [P1] <non-empty one-line step>
- [P2] <non-empty one-line step>
- [P3] <optional; continue sequentially through at most P6>
## Verification
- <non-empty one-line verification>
The pending REVIEW content must be exactly this deterministic seed:
- <one to three non-empty one-line verification bullets>
Omit optional step lines that are not needed. The pending REVIEW must use exactly the Plan's P1..Pn inventory in order and exactly this grammar:
# Review
## Worker Item Status
- P1: pending
- P2: pending
- P3: pending only when P3 exists in PLAN; continue for every chosen Plan id
## Worker Changes
Pending worker execution.
@ -64,7 +73,9 @@ Pending worker execution.
Pending worker verification.
## Deviations
None recorded.`, paths.PlanPath, paths.ReviewPath)
None recorded.
Do not copy the explanatory optional-status text into REVIEW. Emit one pending status line for every and only the actual Plan ids.`, paths.PlanPath, paths.ReviewPath)
default:
return "", fmt.Errorf("selector provider instruction state is invalid")
}

View file

@ -270,6 +270,28 @@ func TestHotPathSelectorOutputLimitIsBounded(t *testing.T) {
}
}
func TestHotPathSelectorPairInstructionCarriesCompactPlanContract(t *testing.T) {
instruction, err := buildHotPathSelectorProviderInstruction("req_compact_plan", selectorInstructionPairWrite)
if err != nil {
t.Fatal(err)
}
for _, fragment := range []string{
"You are the Planner.",
"Analyze the immutable user task first.",
"Preserve every explicit requirement, constraint, deliverable, and acceptance condition.",
"The Worker must be able to implement without rediscovering requirements",
"Make the final Plan step require the Worker to replace the exact REVIEW path",
"actual verification evidence",
} {
if !strings.Contains(instruction, fragment) {
t.Fatalf("selector instruction omitted compact Plan contract %q: %s", fragment, instruction)
}
}
if strings.Contains(instruction, "deterministic seed") || strings.Contains(instruction, "exactly following two-step") {
t.Fatalf("selector instruction retained seed-oriented Plan guidance: %s", instruction)
}
}
func TestOpenAIHotPathResponsesOperationUsesResponsesDecoder(t *testing.T) {
body := []byte(`{"id":"resp-selector","model":"gpt-5.6-terra","status":"completed","output":[{"type":"function_call","id":"fc-1","call_id":"call-1","name":"bash","arguments":"{\"command\":\"true\"}"}],"usage":{"input_tokens":10,"output_tokens":3}}`)
stage, err := decodePresetTunnelBody(body, "application/json", "openai", string(config.OperationResponses), string(config.ProtocolDriverOpenAIChat))