Skip to content

fix: upgrade turbo to 2.6.1 and fix pre-push hook hangs (PRD-5356)#951

Merged
amikofalvy merged 3 commits into
mainfrom
PRD-5356
Nov 12, 2025
Merged

fix: upgrade turbo to 2.6.1 and fix pre-push hook hangs (PRD-5356)#951
amikofalvy merged 3 commits into
mainfrom
PRD-5356

Conversation

@amikofalvy

Copy link
Copy Markdown
Collaborator

Summary

This PR upgrades turbo to 2.6.1 and implements automatic timeout detection and cache clearing in the pre-push hook to fix hangs that previously required manual terminal restarts and cache clearing.

Changes

  • Upgraded turbo: From ^2.5.5 to ^2.6.1 (latest stable)
  • Pre-push hook improvements:
    • Added 5-minute timeout detection
    • Automatic turbo cache clearing on timeout
    • Automatic retry after cache clear
    • Improved process group termination to prevent zombie processes
    • Better error messages and troubleshooting guidance
  • Fixed clean:turbo command: Changed from non-existent turbo clean to rm -rf .turbo
  • Turbo config: Added explicit outputs: [] to check:husky task

How It Works

  1. Pre-push hook runs pnpm check:husky with timeout monitoring
  2. If command hangs (exceeds 5 minutes), it:
    • Detects the hang
    • Kills the process group (including all child processes)
    • Clears the turbo cache automatically
    • Retries once
  3. If retry succeeds, push continues normally
  4. If retry also times out, provides helpful error messages

Testing

  • Turbo configuration validated with dry-run
  • Pre-push hook syntax validated
  • All files linted successfully

Related

Fixes PRD-5356

- Automatically detect server URL from request headers (x-forwarded-host, x-forwarded-proto, host)
- Fallback to existing AGENTS_MANAGE_API_URL and AGENTS_RUN_API_URL environment variables
- Enables valid OpenAPI specs with correct server URLs in deployed environments
- Upgrade turbo from ^2.5.5 to ^2.6.1
- Add timeout detection (5 minutes) to pre-push hook
- Automatically clear turbo cache and retry on timeout
- Improve error handling and process group termination
- Fix clean:turbo command (was using non-existent turbo clean)
- Add explicit outputs to check:husky task in turbo.json

Fixes PRD-5356
@changeset-bot

changeset-bot Bot commented Nov 12, 2025

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 8b4d115

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Nov 12, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
agents-docs Ready Ready Preview Comment Nov 12, 2025 9:08pm
agents-manage-api Ready Ready Preview Comment Nov 12, 2025 9:08pm
agents-manage-ui Ready Ready Preview Comment Nov 12, 2025 9:08pm
agents-run-api Ready Ready Preview Comment Nov 12, 2025 9:08pm

@claude

claude Bot commented Nov 12, 2025

Copy link
Copy Markdown
Contributor

Claude finished @amikofalvy's task —— View job


PR Review: Turbo Upgrade and Pre-Push Hook Improvements

Todo List:

  • Analyze changed files and understand the scope
  • Review Turbo upgrade compatibility and changes
  • Evaluate pre-push hook timeout/retry implementation
  • Check for security and architectural considerations
  • Assess system-wide impacts and consistency
  • Provide comprehensive feedback and recommendations

Review Summary

This PR addresses a critical developer experience issue by upgrading Turbo and implementing robust timeout handling in the pre-push hook. The changes are well-architected and follow defensive programming principles.

🟢 What Works Well

Turbo Upgrade (2.5.5 → 2.6.1)

  • Clean minor version bump: The upgrade follows semantic versioning appropriately
  • Fixed clean:turbo command: Correctly replaced non-existent turbo clean with rm -rf .turbo at package.json:38
  • Proper configuration: Added explicit outputs: [] to check:husky task in turbo.json:155

Pre-Push Hook Improvements

  • Robust timeout detection: 5-minute timeout with proper process monitoring (.husky/pre-push:27-91)
  • Graceful process termination: Implements proper signal handling (TERM → KILL) with process group cleanup
  • Automatic recovery: Cache clearing and retry logic addresses the root cause of hangs
  • Clear user feedback: Excellent progress reporting and troubleshooting guidance

🟡 Areas for Consideration

Critical: Process Group Management

Issue: Lines 70-84 in .husky/pre-push have a potential race condition

if [ -n "$pgid" ] && [ "$pgid" != "1" ]; then
  kill -TERM -$pgid 2>/dev/null || kill $cmd_pid 2>/dev/null || true

Risk: If pgid becomes empty between the check and use, kill -TERM - could fail unexpectedly.

Recommendation: Add additional validation:

if [ -n "$pgid" ] && [ "$pgid" != "1" ] && [ "$pgid" != "" ]; then
  kill -TERM "-$pgid" 2>/dev/null || kill $cmd_pid 2>/dev/null || true

Security: Process Termination Scope

Concern: The process group termination (kill -TERM -$pgid) could potentially affect other processes if process group detection fails.

Recommendation: Consider adding a safeguard to verify the process group contains only expected processes before termination.

System Design: Timeout Configuration

Current: Hard-coded 5-minute timeout and single retry
Enhancement: Consider making these configurable via environment variables:

TIMEOUT_SECONDS=${TURBO_TIMEOUT:-300}
RETRY_COUNT=${TURBO_RETRY_COUNT:-1}

🔴 Required Actions

Changeset Missing

The changeset-bot indicates no changeset was created. This is a patch-level change that should include:

  • Turbo version upgrade
  • Pre-push hook reliability improvements

Testing Gaps

Missing: No automated tests for the new timeout/retry logic
Risk: The shell script logic is complex and prone to edge cases
Recommendation: Consider adding basic integration tests or at minimum, manual testing documentation

🏗️ Architectural Considerations

Consistency with Existing Patterns

  • ✅ Follows existing shell script conventions in the codebase
  • ✅ Maintains backward compatibility with existing git hooks
  • ✅ Uses established error codes (124 for timeout, matching timeout command)

Extensibility

The timeout/retry framework could be extracted into a reusable utility function for other long-running commands in the codebase.

Performance Impact

  • ✅ Minimal overhead: 5-second check intervals are reasonable
  • ✅ Process monitoring is lightweight
  • ⚠️ Consider reducing check interval to 1-2 seconds for more responsive feedback

🎯 Recommendations

High Priority

  1. Add changeset for proper version tracking
  2. Fix process group race condition in kill logic
  3. Test edge cases: Very quick failures, process group edge cases

Medium Priority

  1. Make timeout configurable via environment variables
  2. Add integration test or documented manual testing procedure
  3. Consider more granular progress reporting (every 30s instead of 60s)

Low Priority

  1. Extract timeout utility for reuse across other git hooks
  2. Add telemetry for timeout/retry frequency to identify patterns

Overall Assessment

This is a well-executed solution to a real developer pain point. The implementation shows good understanding of shell scripting best practices and defensive programming. The timeout/retry logic is sophisticated and the user experience improvements are excellent.

The code quality is high with proper error handling, clear documentation, and thoughtful UX. The only significant concerns are the process group race condition and missing changeset.

Recommendation: Approve with minor fixes for the process group handling and changeset addition.


@amikofalvy amikofalvy merged commit 95a0131 into main Nov 12, 2025
8 of 9 checks passed
@amikofalvy amikofalvy deleted the PRD-5356 branch November 12, 2025 21:08
specimba pushed a commit to specimba/agents that referenced this pull request Jun 5, 2026
…ep#3305)

* spec(agents-core): sub-agent output contract — decisions locked

Locks D-A through D-F on specs/2026-05-12-sub-agent-output-contract/:

- D-A: outputContract object on SubAgentConfig (Option B over flat allowText)
- D-B: onViolation 'retry' default with retryBudget 1
- D-C: new output_contract jsonb column on sub_agents (additive migration)
- D-D: per-invocation override deferred to Future Work (NG1)
- D-E: inferred allowText:false default when any structured capability declared
- D-F: stopWhen stays iteration-budget; structured-emission stop derived at runtime

Cascade-resolves Q-E (streaming partial frames structurally prevented),
Q-F (contract is per-config across A2A), Q-G (init-time validation).

Status: Decisions locked — pending audit (Steps 6-7). No code changes.

* spec: incorporate PR inkeep#951 review on sub-agent output contract

Reverse D-E to explicit-only. allowText now defaults to true and
allowText: false is an explicit author opt-in. The framework no longer
infers it from declared capabilities, so the feature is purely additive.
This resolves the review's D-E scope and deprecation-phase findings.

Add FR10 for the transfer/delegate-only enforcement hole. hasStructuredOutput
is derived from dataComponents alone, so violation detection was unreachable
for transfer/delegate-only agents. A broadened hasContractEnforcement flag
fixes it.

Move the durable-workflow path into scope, define the ContractViolationError
client error shape, retype requireTransfer to a string id, and correct the
SubAgentApiInsertSchema citation.

* spec: add requireArtifact to the output contract

Add requireArtifact to OutputContract so a sub-agent can require an
artifact component as its terminal output, alongside the existing
requireComponent (data) and requireTransfer.

Lock decision D-G: the require* fields cover terminal output shapes
(data component, artifact component, transfer) and not workflow steps
(delegate, tool call). Add FR11 for init-time validation and broaden
FR4 so an unsatisfied require* field is a runtime violation routed
through onViolation. Add a STOP_IF for the artifact-only-agent
schema-injection caveat.

* spec: widen FR10 enforcement flag for require* fields

The hasContractEnforcement flag only fired for allowText: false or
agents with data components. An allowText: true agent constrained
solely by requireTransfer or requireArtifact would have its require*
constraint silently unenforced. Widen the flag to also fire when any
require* field is set.

Also document that allowText and require* are orthogonal: allowText
governs whether prose is permitted, require* mandates a terminal
emission. allowText: true with requireComponent is a valid combination.

* spec: clarify the two contract enforcement modes

Make explicit that allowText: false is structural prevention (the
schema drops the Text variant, so it cannot be violated) while the
require* fields are detection-based (the framework inspects the
emitted structured parts after generation and applies onViolation
on a miss). The presence of a text part does not affect the require*
check, which inspects structured parts rather than parsing prose.

* spec: add plain-language summary section

Add an unnumbered Summary at the top of SPEC.md giving a plain-language
overview of the contract fields, the two enforcement modes, and the
opt-in backwards-compatibility guarantee, so a reader is oriented before
the dense numbered sections. Update the status line to D-A through D-G.

* spec: incorporate claude re-review precision items

The re-review (Approve with Suggestions) confirmed all five Major
findings resolved. Take the remaining Minor/Consider items:

- FR4 gains clause (c): an empty turn under allowText: false is a
  violation; resolves the open "is empty allowed" matrix cell.
- New shadow path: structured output plus prelude text takes the
  structured-success path, text discarded, not a violation.
- Fix the executionHandler catch citation (676-680 was a logger.warn);
  re-anchor to the iteration-loop try at 183 as a new catch branch.
- Note that contract retries are inner to runGenerate.
- requireTransfer accepts string or SubAgentInterface, normalized to id.
- Enumerate the durable-path threading steps.
- Document the two enforcement paths under allowText: false.

* feat(agents-core,agents-sdk): output contract data layer

Add the OutputContract type, Zod schema, DB column, and SDK field for the
sub-agent output contract (spec specs/2026-05-12-sub-agent-output-contract).

- agents-core: OutputContractSchema (looseObject, forward-compatible) and
  OutputContract type; output_contract jsonb column on sub_agents;
  SubAgentInsertSchema and the DAL upsert carry the field.
- agents-sdk: OutputContract interface on SubAgentConfig; requireTransfer
  accepts a string id or a SubAgentInterface, normalized to the id on persist.
- Drizzle migration 0021 adds output_contract (nullable). It also drops a
  stale default on webhook_destinations.headers, pre-existing schema drift
  on main that drizzle-kit swept into this migration.

* feat(agents-api): output contract structural enforcement

Wire the sub-agent output contract into the runtime structural-prevention
path (spec FR2, FR3, FR10).

- agent-types.ts: outputContract on AgentConfig; resolvedAllowText on
  AgentRunContext.
- Agent.ts: resolve resolvedAllowText in the constructor and gate the
  synthetic Text data component on it (FR2) - allowText: false drops Text
  from the response schema.
- model-config.ts: derive hasContractEnforcement (FR10) so the violation
  path reaches transfer/delegate-only and require-only agents.
- generate.ts: set toolChoice 'required' for a transfer/delegate-only
  agent under allowText: false (FR3).
- generateTaskHandler.ts and agentExecutionSteps.ts: thread outputContract
  from the sub-agent schema into both Agent construction paths.
- New ContractViolationError class for the onViolation reject path.

* feat(agents-api,agents-sdk): output contract violation handling

Implement onViolation routing (FR4) and init-time validation (FR6/FR7/FR11).

- generate.ts: detect a contract violation after generation
  (isContractViolation) and route through onViolation - 'warn' logs and
  surfaces fallback text, 'reject' and 'retry' throw ContractViolationError.
  OTel attributes agent.output_contract.{violated,policy_applied} are set.
- executionHandler.ts: a ContractViolationError is surfaced as an error
  operation carrying the code CONTRACT_VIOLATION.
- subAgent.ts: SubAgent.init() validates requireComponent / requireArtifact /
  requireTransfer against the declared components and transfers, throwing
  before persistence.

The 'retry' policy currently rejects immediately (zero retries); the
bounded retry re-invocation loop is a tracked follow-up.

* test(agents-api,agents-sdk): output contract unit tests + changeset

- isContractViolation detection matrix (allowText:false, requireComponent).
- ContractViolationError structured fields.
- SubAgent.init() fail-fast for undeclared requireComponent / requireArtifact /
  requireTransfer (FR6/FR7/FR11).
- Changeset: minor bump for agents-core, agents-sdk, agents-api.

* docs(spec): record output contract implementation status

Note what landed on PR inkeep#951 (data layer, structural runtime, violation
handling, init validation, unit tests) and what is a tracked follow-up
(FR4 retry loop, integration suite, docs/cookbook).

* refactor(agents-api): consolidate output contract logic, complete detection

Extract all output-contract runtime logic into a dedicated
agents/output-contract.ts module (thematic with model-config.ts):
resolveAllowText, deriveContractEnforcement, resolveContractToolChoice,
isContractViolation, enforceOutputContract. Agent.ts, model-config.ts,
and generate.ts now delegate to it instead of inlining the logic.

Complete the runtime violation detection (previously partial):
- requireArtifact, via the ArtifactCreate_<name> data component.
- requireTransfer, via the transfer_to_<id> tool call.
- transfer/delegate-only agents emitting text instead of a tool call.

Extract the ArtifactCreate_ data-component name prefix as a shared
ARTIFACT_CREATE_PREFIX constant so the convention is single-sourced.

* docs(spec): document requireArtifact create-only limit

requireArtifact runtime detection is satisfied by creating the artifact
(the ArtifactCreate_<name> data component), not by emitting a generic
Artifact reference to a pre-existing artifact. The reference component
does not carry the artifact type, so type-precise reference matching
needs artifact-registry resolution and is recorded as Future Work.
Also notes the recommended step-loop approach for the deferred retry.

* spec: design the FR4 retry mechanism (D-H), defer implementation

/spec pass on the output-contract retry loop. Source-verified ai@6.0.14's
multi-step loop: the do/while continues only after a tool-call step and
stopWhen can only stop early, never force continuation. The headline
violation is a no-tool-call terminal step, so prepareStep/stopWhen cannot
drive the retry. The retry must be a bounded outer re-invocation loop.

Adds decision D-H, the retry design under §15 Future Work (Explored),
and evidence/ai-sdk-loop-semantics.md. Implementation deferred to its
own PR; onViolation:'retry' rejects immediately until then.

* fix(agents-api): contract violation surfacing + CI test fixture

CI fix: the new output_contract column makes SubAgentSelectSchema require
the key; the schema-wrappers test fixture was missing it.

Major (review): a ContractViolationError thrown from runGenerate is caught
at the generation-wrapping boundary (generateTaskHandler standard path,
agentExecutionSteps durable path), never at executionHandler, so the
executionHandler instanceof branch was dead code. generateTaskHandler now
sets a contract_violation A2ATaskErrorType on the failed result;
agentExecutionSteps emits an SSE error op with CONTRACT_VIOLATION; the dead
executionHandler branch is removed.

Also: tests for enforceOutputContract / deriveContractEnforcement /
resolveContractToolChoice; remove the JSDoc block (No Comments); warn log
carries conversationId + policy; spec updated to the real mechanism.

* fix(agents-api): update OpenAPI snapshot, resolve review inkeep#5 findings

CI: the outputContract field changed the generated OpenAPI spec; the
committed __snapshots__/openapi.json is regenerated. Full agents-core /
agents-api / agents-sdk suites run clean.

- enforceOutputContract throws and reports the effective policy (reject)
  rather than the misleading configured 'retry' + attemptedRetries 0,
  since retry rejects immediately until the retry loop lands (D-H).
- generateTaskHandler's durable-approval catch no longer swallows a
  ContractViolationError that co-occurs with a pending approval.
- Added the missing deriveContractEnforcement / resolveContractToolChoice
  / enforceOutputContract test cases (output-contract.test.ts now 26).

* feat(agents-manage-ui): output contract section in the sub-agent editor

Adds an Output contract section to the sub-agent editor so no-code
builders can declare structured-only emission:

- allowText as a "Structured output only" checkbox.
- requireComponent / requireArtifact / requireTransfer inputs.
- onViolation select offering Reject and Warn only - retry is not
  selectable while it rejects immediately (D-H), noted in the field
  description. retryBudget is omitted (meaningless without retry).

The field round-trips through the existing spread-based form and
full-agent upsert path. OutputContractFormSchema + form validation
tests added; agents-manage-ui changeset.

* docs(agents-docs): add the Output Contract SDK doc page

New typescript-sdk/output-contract.mdx covering the contract fields, the
two enforcement modes (structural allowText vs detection-based require*),
onViolation, init-time validation, and the requireArtifact create-only /
retry-deferred notes. Added to the typescript-sdk nav.

Completes PR inkeep#951 as a full-stack change: SDK, core, DB, runtime,
Manage UI, docs, tests, changesets.

* fix(agents-manage-ui): cohesive Output section + dirty-state fix

The bolted-on output-contract section made the sub-agent form spuriously
dirty on load (apiToFormValues carried outputContract as the DB null,
but the registered fields materialized it as an object), breaking the
agent.cy.ts dirty-state E2E.

- apiToFormValues normalizes outputContract to a consistent 6-key object
  and OutputContractFormSchema is a z.strictObject with z.preprocess
  null->undefined per field - mirroring how stopWhen is handled.
- dataComponents, artifactComponents, and the contract are now one
  cohesive "Output" section instead of three independent peers.
- requireComponent / requireArtifact / requireTransfer are selects
  driven by the declared components / transfer targets, not free-text
  inputs - removing the typo-to-init-error footgun.

* fix(agents-manage-ui): serialize outputContract on save (BLOCKING)

editorToPayload (the form->API serializer) never included outputContract
in the sub-agent payload, so every contract configured through the UI was
silently discarded on save.

- editorToPayload now serializes outputContract via a buildOutputContract
  helper that drops unset/default fields (allowText kept only when false)
  and omits the contract when empty - mirroring the conditional stopWhen
  spread. 3 round-trip tests added to serialize.test.ts.
- docs: output-contract.mdx onViolation type corrected to include
  'retry' (the SDK type accepts it); added a "Handling a rejection"
  section on the contract_violation task type / CONTRACT_VIOLATION code.

* refactor(agents-manage-ui): consolidate Output section into Guardrails modal

Collapse the seven stacked controls in the sub-agent editor's Output
section into a single modal behind a compact summary card, and rename
the section to Guardrails. The modal holds the component and artifact
pickers plus the contract rules (structured-only emission, require
component/artifact/transfer, on-violation). Form fields edit the agent
form live, consistent with the rest of the sidepane.

* feat(agents): output contract require-all lists + spec amendment

Amend the sub-agent output contract per spec decisions D-I and D-J.
requireComponent and requireArtifact become string[] with require-all
semantics: every named component or artifact must appear in the
response. requireTransfer becomes a boolean, since a turn transfers
exactly once and cannot name a target.

Updates the agents-core OutputContractSchema, the SDK OutputContract
type and SubAgent.init() validation, runtime violation detection in
output-contract.ts, the OpenAPI snapshot, and the Guardrails editor
(multi-select require rows, transfer toggle, "Text-free responses"
rename). SPEC.md records D-I, D-J, and FR12. The FR12 prompt-injection
surface is specced but not yet implemented.

* feat(agents-api): surface output contract in the system prompt (FR12)

Implements spec decision D-J. When a sub-agent has a resolved output
contract, PromptConfig renders an <output_contract> section into the
system prompt stating the active rules: no free-text narration when
allowText is false, the required data components and artifacts, and a
required transfer. The section is omitted entirely when no contract is
set, so the common path pays no prompt-size cost.

This is a prevention layer that steers the model on attempt 1; it
complements, and does not replace, the runtime detection-and-retry in
output-contract.ts. The resolved contract and resolvedAllowText thread
from AgentRunContext through SystemPromptV1 into PromptConfig.assemble.

* refactor(agents): dedup OutputContract type and contract helpers

OutputContract was defined twice — a hand-written interface in the SDK
and the Zod-inferred type in agents-core. After D-I made them
field-identical, the SDK now re-exports the agents-core type so there
is a single definition.

Also collapses repetition in the contract code: a shared
optionalContractBoolean preprocessor for allowText and requireTransfer
in the form schema, and an allRequiredPresent helper for the two
require-all checks in output-contract.ts.

* feat(agents-manage-ui): require components via chip toggle in Guardrails

Reworks the Guardrails modal so requirements are expressed on the
inventory chips themselves instead of separate dropdowns. Clicking a
selected component or artifact chip toggles whether it is required;
required chips are highlighted in the primary color. The separate
"Require component" / "Require artifact" selects are removed.

The structured-output control becomes an "Include text" checkbox
(checked by default) — unchecking it is the text-free contract.

ComponentSelector and SelectedComponents gain two opt-in props
(requiredComponents, onComponentClick) so the chip highlight and click
behavior are additive — existing callers are unaffected. The two
inventory fields are rendered from one config-driven map rather than
duplicated blocks.

* fix(agents-manage-ui): make the required-chip highlight solid

The 10%-tint fill read as too subtle against the dark panel. A
required chip now uses a solid primary fill so it is unmistakable.

* fix(agents-manage-ui): required chip invisible in dark mode

The code Badge variant carries dark:bg-muted/50; tailwind-merge keeps
it alongside an unprefixed bg-primary, so in dark mode the required
chip stayed grey and the dark primary-foreground text disappeared into
it. Add dark:bg-primary to override the variant's dark background.

* fix(agents-manage-ui): bolder required chip with a visible remove icon

The ghost Button variant hard-codes text-muted-foreground, so the ×
stayed low-contrast grey on the solid primary chip. Required chips now
use semibold text and force the × to the chip's foreground color.

* fix(agents-core): persist sub-agent outputContract on full-agent save

The full-agent upsert path builds each sub-agent's data field by field
and never included outputContract, so every contract configured in the
editor was silently dropped on save — the UI serialized it correctly
but the DAL orchestrator discarded it. Pass outputContract through to
upsertSubAgent.

* fix(agents-core): return sub-agent outputContract on full-agent read

getFullAgentDefinition rebuilds each sub-agent object field by field
and omitted outputContract, so a saved contract was dropped on
read-back even after the write path was fixed. Return it from the
assembled sub-agent object.

* fix(agents-core): persist outputContract on full-agent UPDATE path

There are two full-agent server functions — createFullAgentServerSide
and updateFullAgentServerSide — each with its own sub-agent loop.
Commit b10c26d54 fixed the create loop; this fixes the update loop,
which is the one editing an existing agent actually runs. Its
upsertSubAgent call also omitted outputContract.

* fix(agents-core): allow clearing a sub-agent outputContract

Removing the last requirement (or the whole contract) sent no
outputContract, so the full-agent update passed undefined to
upsertSubAgent — and Drizzle's .set() skips undefined, leaving the old
contract in the column. A contract could be set but never unset.

Coerce the full-agent upsert to write null when the contract is
absent, and make SubAgentInsertSchema.outputContract nullable to match
the nullable column so null type-checks through the chain.

* feat(agents-api): report which rule an output-contract violation broke

The violation error said only "violated its output contract" — it
never named the offending rule, so a failure was undiagnosable.

Add getContractViolation, which returns a human-readable reason: which
require* rule failed, what was expected, and what the response
actually emitted (or that free text was disallowed). The reason is
threaded into ContractViolationError's message, the warn log, and a
span attribute. isContractViolation stays as a boolean wrapper.

* fix(agents-core): drop require* entries for undeclared components

A requirement could outlive the component it pointed at: delete a data
component from a sub-agent and outputContract.requireComponent kept the
stale name, leaving an unsatisfiable contract that rejected every turn.

On full-agent save, reconcile each sub-agent's outputContract — drop
requireComponent / requireArtifact entries that name a component or
artifact the sub-agent no longer declares. This self-heals stale
contracts (any re-save fixes them) and covers every removal path, not
just the editor's in-modal prune.

* refactor(agents-core): extract loadComponentNameMaps helper

The component/artifact id-to-name map building was duplicated verbatim
in both createFullAgentServerSide and updateFullAgentServerSide.
Extract it into one helper, which also fetches the two lists in
parallel rather than sequentially.

* fix(agents-api): count marker-created artifacts toward requireArtifact

requireArtifact only checked for the structured ArtifactCreate_<name>
data component, so an artifact created via the <artifact:create> text
marker — a first-class, supported path — was missed, and the contract
rejected a turn that had genuinely produced the required artifact.

getContractViolation now also recognizes <artifact:create> markers,
reusing ArtifactParser's own parser (parseCreateAnnotations, exposed
statically) rather than re-implementing the marker regex.

* docs(spec): amend output-contract spec with D-K and D-L

D-K — requireArtifact is satisfied by an artifact reference, not only
creation; both the XML markers and the structured data-component forms
count. Amends FR11, reverses the 2026-05-21 create-only call, promotes
the type-precise reference-matching item out of Future Work.

D-L — under allowText: false, artifacts emit as structured data
components (ArtifactCreate_ / ArtifactReference) rather than the
in-text XML markers. Adds FR13, resolves the open §16 STOP_IF about
artifact-only agents having no text-free emission channel.

* feat(agents-api): structured artifacts under allowText:false (D-L/FR13)

An artifact-only sub-agent (artifactComponents, no dataComponents) had
hasStructuredOutput=false, so it never entered structured-output mode
and could only emit artifacts via the in-text <artifact:create> marker
— which allowText:false excludes, leaving it no text-free channel.

Broaden hasStructuredOutput: an artifact-only sub-agent with
allowText:false now uses the structured schema, where schema-builder
already injects the ArtifactCreate_/ArtifactReference variants.

* feat(agents-api): requireArtifact counts references, not just creation (D-K)

A requireArtifact entry is now satisfied when the response references
an existing artifact, not only when it creates one — covering both the
<artifact:ref> text marker and the structured Artifact reference
component. A reference carries only an artifact id, resolved to a type
against ctx.config.artifacts.

Reuses ArtifactParser: a generic parseAttrs is extracted and a static
parseRefIds added alongside parseCreateAnnotations, so the marker parse
is not re-implemented.

* feat: reject requireTransfer combined with requireComponent/requireArtifact (FR14)

A transfer and a component/artifact are different terminal output
shapes (D-G) — a turn ends one way, so requiring both is
unsatisfiable. Reject the combination in two layers: an
OutputContractSchema refine in agents-core (the API path) and
validateOutputContract in agents-sdk (init-time fail-fast with a clear
message). requireComponent + requireArtifact together stay valid.

Amends the spec with FR14.

* fix(agents-api): restore template file imports lost in FR12

The FR12 commit inlined the six prompt-template files as huge string
constants so the {{OUTPUT_CONTRACT_SECTION}} placeholder could be added
without touching system-prompt.xml. That left templates/v1/ as dead files
diverged from the live source.

Restore the ?raw imports and add the placeholder to
system-prompt.xml so the template directory is the source of truth again.
Also drop artifact-retrieval-guidance.xml, a pre-existing dead duplicate
of the .md variant.

* fix(agents): address PR review findings on output contract

- output-contract.ts: include data-component artifacts (ArtifactCreate_<name>) in
  the requireArtifact violation diagnostic so the message no longer reports
  "produced [none]" when the artifact was created via the structured path.
- selected-components.tsx: add aria-pressed to the chip toggle and an
  aria-label on the remove button so the chip's required state and remove
  affordance are exposed to assistive tech.
- guardrails-section.tsx: drop "Retry is not yet available" from the
  on-violation description so the UI does not advertise an absent option.
- validation.ts: align OutputContractFormSchema with core: looseObject for
  forward compatibility, nullish wrapper so API null round-trips, and
  retryBudget bounded with .int().nonnegative(). serialize.ts widens
  buildOutputContract to accept null alongside undefined.
- agentFull.ts: skip the listDataComponents / listArtifactComponents pair
  when no sub-agent has an outputContract; reconcileOutputContract already
  early-returns null in that case so empty maps are safe. Export
  reconcileOutputContract for direct testing and drop the orphan stale
  JSDoc that was left above loadComponentNameMaps.

* test(agents): cover reconcile + allowText reason and fix stale Agent assertions

- agentFull.reconcileOutputContract.test.ts: new file with 7 tests covering
  the function's branches (null contract, retained/partial/full-drop for
  requireComponent and requireArtifact, allowText/onViolation pass-through,
  and an input-mutation guard).
- output-contract.test.ts: add a getContractViolation test asserting the
  reason names allowText when free-text-only output violates allowText:false.
- Agent.test.ts: update four buildSystemPrompt .toHaveBeenCalledWith
  assertions that pre-dated the FR12 / appPrompt additions and were already
  failing on this branch.

* docs(spec): correct stale catch-site citation in §11

The §11 file list pointed at handlers/executionHandler.ts as the catch
site for ContractViolationError, but that file does not reference the
error. The standard-path catch lives in generateTaskHandler.ts (which
maps it to a 'contract_violation' task failure); the durable-path catch
in agentExecutionSteps.ts was already listed correctly on the next line.

* fix(agents-api): tolerate NoObjectGeneratedError when the model transferred

Agents that declare dataComponents set Output.object() on the AI SDK call,
which forces every turn to emit a structured object. When such an agent
transfers or delegates instead, the AI SDK rejects with NoObjectGenerated-
Error and the run crashes — even though the transfer was the legitimate
alternative the model was instructed to take (e.g. requireTransfer: true).

resolveGenerationResponse now treats that specific rejection as "output
is undefined" when the steps include a transfer_to_* or delegate_to_*
tool call. Every other rejection (parse errors, schema mismatches,
streaming faults, network errors) still throws.

The thrown error is also reworked into a GenerationResponseError that
leads with an action-oriented hint, preserves the original error as
cause, and appends field / finishReason / toolCalls for triage. The hint
varies by failure mode:

- "no structured output, no transfer/delegate" -> points at
  outputContract.requireComponent and allowText:false as the way to
  enforce structured output.
- "non-routing tool call but no object" -> points at removing
  dataComponents or routing via transfer/delegate.
- "schema mismatch / parse failure" -> points at simplifying the
  schema or using a more capable model.
- other fields rejecting -> calls out streaming/network/SDK fault.

Adds tolerance tests for transfer + delegate, plus tests for each hint
branch and for non-output-field failures. Existing tests updated to the
new diagnostic format (field=X cause=Y).

* test(agents-api): cover D-L/FR13, D-K edge cases, and parseRefIds

Three coverage gaps surfaced in PR inkeep#951 review inkeep#18.

model-config.hasStructuredOutput.test.ts (new): pins the D-L/FR13 branch
in configureModelSettings — hasStructuredOutput becomes true for
artifact-only sub-agents under allowText:false. Five tests cover both
sides of the && and confirm the legacy dataComponents path still
applies. Guards against a regression where the && becomes || or the
resolvedAllowText check is dropped, which would silently route
artifact-only agents to the wrong model.

output-contract.test.ts: two new getContractViolation cases pin the D-K
guards — a ref to an artifact id absent from ctx.config.artifacts and a
structured Artifact data component with artifact_id: null should both
still fail requireArtifact. The guards live at output-contract.ts:88-91
and would be easy to "broaden" by accident.

ArtifactParser.parseRefIds.test.ts (new): six standalone tests for the
D-K reference-detection regex — no refs, single ref (self-closing and
non), multi-ref, missing id attribute, single-quoted attrs. The
multi-ref case is the primary D-K use case (multiple citations in one
response) and was previously only exercised indirectly with one ref.

* test(agents-api): cover remaining hint/diagnostic branches from review inkeep#19

- resolveGenerationResponse.test.ts: add a test for the generic-output
  fallback hint (unrecognized error name → "See the cause for details"),
  plus a tolerance test using the bare NoObjectGeneratedError name to
  exercise the no-prefix branch of isNoObjectGeneratedError.
- output-contract.test.ts: add a getContractViolation case asserting the
  requireArtifact diagnostic includes the produced artifact type after
  stripping the ArtifactCreate_ prefix; plus an enforceOutputContract case
  for explicit onViolation: "retry" (currently aliased to reject until
  D-H lands).
- agent-types.ts: document why isNoObjectGeneratedError compares names by
  string rather than instanceof, and which AI SDK version supplies the
  current name set — so a future SDK upgrade triggers a review of this
  guard.

* docs(agents-docs): align output-contract page with shipped behavior

- Drop "retry" from documented onViolation values and remove the
  retryBudget row. Retry semantics are not implemented (the runtime
  aliases "retry" to "reject" today), so documenting them misleads users
  into thinking they get retries when they do not. onViolation is now
  documented as "reject" | "warn" defaulting to "reject".
- Fix the stale requireArtifact note: D-K landed in this branch and
  reference paths (<artifact:ref> markers and structured Artifact data
  components) now satisfy requireArtifact alongside creation.
- Document FR14: requireTransfer cannot be combined with requireComponent
  or requireArtifact. Mention it both in the field table and the Notes
  section so users do not stumble onto the validation error blind.
- Update the rejection-handling paragraph to mention that error messages
  now include the specific rule that was violated (matches the
  ContractViolationError.message shape on this branch).
- Add cross-links from the data-components and artifact-components SDK
  pages to the output-contract page so the discovery path "I declared a
  component; how do I make the model actually use it?" is one click.

* docs(agents-docs): drop internal mechanism details from output-contract page

The page is for SDK users; they shouldn't have to know about
Output.object(), schema unions, "Text variants", artifact markers, or
the artifact-id registry. Rewrite the two sections that leaked these:

- Drop the "Two enforcement modes" section. The structural-vs-detection-
  based framing was an implementation distinction, not something a user
  acts on. Keep one sentence in its place clarifying that allowText and
  the require* fields are independent.
- In Notes, rewrite the requireArtifact bullet to say "creating or
  referencing the named artifact" without naming the internal markers
  (<artifact:create>, <artifact:ref>) or data-component prefix
  (ArtifactCreate_). Users just declare the artifact component; the
  framework recognises both forms.
- In Validation and Notes, replace "SubAgent.init()" and "the underlying
  schema" with "registration time" so the doc doesn't bind to internal
  function names.

No behavior change. No schema change.

* docs(agents-docs): nest output-contract under structured-outputs and link bidirectionally

Placement: move output-contract.mdx into structured-outputs/ so the page
lives in the section a reader is in when they discover the need for it.
The page covers transfers and prose too, but every discovery path runs
through declaring components/artifacts first, and the section nav now
reads: data-components, artifact-components, status-updates,
output-contract.

Links:
- output-contract field-table entries now link out to data-components,
  artifact-components, and agent-relationships so each require* field
  points at the concept it constrains.
- data-components and artifact-components keep their "Enforcing X
  emission" sections but the link now resolves to the new nested URL.
- agent-relationships gets a "Requiring a transfer" closing section
  pointing back at the output contract for the requireTransfer pattern,
  closing the loop from the transfer/delegate side.

Nav metadata updated in both typescript-sdk/meta.json (output-contract
removed from the top-level list) and structured-outputs/meta.json
(output-contract added after status-updates).

No content rewrites; pure structural + linking.

GitOrigin-RevId: fbb40487628ce533955c1ee207755926057b75e1

Co-authored-by: tim-inkeep <132074086+tim-inkeep@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant