@agent-native/core 0.224.0-nightly-20261104110058 → 0.224.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/dist/action-frame.d.ts +35 -1
  2. package/dist/action-frame.js +50 -0
  3. package/dist/action.d.ts +40 -9
  4. package/dist/action.js +71 -16
  5. package/dist/adapters/cli/shell-adapter.js +4 -1
  6. package/dist/agent/engine/builder-engine.js +2 -1
  7. package/dist/agent/engine/inline-tool-result-images.d.ts +14 -0
  8. package/dist/agent/engine/inline-tool-result-images.js +165 -0
  9. package/dist/agent/optional-placeholders.d.ts +20 -0
  10. package/dist/agent/optional-placeholders.js +191 -0
  11. package/dist/agent/production-agent.d.ts +2 -0
  12. package/dist/agent/production-agent.js +15 -180
  13. package/dist/agent/types.d.ts +1 -0
  14. package/dist/application-state/handlers.d.ts +3 -3
  15. package/dist/audit/record.js +4 -1
  16. package/dist/cli/build-failure-report.d.ts +1 -0
  17. package/dist/cli/build-failure-report.js +8 -1
  18. package/dist/cli/create.d.ts +12 -8
  19. package/dist/cli/create.js +93 -94
  20. package/dist/cli/framework-package-versions.d.ts +10 -0
  21. package/dist/cli/framework-package-versions.js +53 -0
  22. package/dist/cli/git-http.d.ts +38 -0
  23. package/dist/cli/git-http.js +540 -0
  24. package/dist/cli/http-proxy.d.ts +3 -0
  25. package/dist/cli/http-proxy.js +21 -0
  26. package/dist/cli/index.js +1 -0
  27. package/dist/cli/sentry-telemetry.js +3 -0
  28. package/dist/cli/template-sync.js +2 -1
  29. package/dist/cli/workspace-dev.d.ts +1 -0
  30. package/dist/cli/workspace-dev.js +37 -3
  31. package/dist/client/use-db-sync.d.ts +14 -0
  32. package/dist/client/use-db-sync.js +100 -5
  33. package/dist/coding-tools/sandbox/local-child-process-adapter.js +2 -0
  34. package/dist/collab/agent-presence.js +8 -4
  35. package/dist/db/client.js +52 -2
  36. package/dist/db/create-get-db.js +52 -13
  37. package/dist/db/ddl-guard.js +7 -2
  38. package/dist/db/note-sql-statement.d.ts +5 -0
  39. package/dist/db/note-sql-statement.js +10 -0
  40. package/dist/db/sql-write-classifier.d.ts +9 -0
  41. package/dist/db/sql-write-classifier.js +165 -0
  42. package/dist/db/store-registry.js +23 -4
  43. package/dist/decisions/providers/jev.js +4 -2
  44. package/dist/decisions/providers/openai.js +4 -2
  45. package/dist/deploy/workspace-deploy.js +17 -8
  46. package/dist/embeddings/index.js +4 -2
  47. package/dist/file-upload/actions/upload-image.d.ts +3 -3
  48. package/dist/file-upload/owned-attachment.d.ts +2 -0
  49. package/dist/file-upload/owned-attachment.js +4 -0
  50. package/dist/framework-package-versions.json +17 -0
  51. package/dist/local-artifacts/index.js +8 -0
  52. package/dist/mcp/analytics.js +3 -3
  53. package/dist/mcp/direct-action-call.js +2 -2
  54. package/dist/mcp/embed-redaction.d.ts +8 -0
  55. package/dist/mcp/embed-redaction.js +21 -0
  56. package/dist/mcp/execute-action-call.js +2 -2
  57. package/dist/mcp/screen-memory-stdio.d.ts +3 -3
  58. package/dist/mcp-client/manager.js +2 -1
  59. package/dist/org/handlers.d.ts +1 -1
  60. package/dist/org/index.d.ts +1 -1
  61. package/dist/org/migrations.d.ts +1 -1
  62. package/dist/outbound-write-observation.d.ts +15 -0
  63. package/dist/outbound-write-observation.js +104 -0
  64. package/dist/progress/routes.d.ts +1 -1
  65. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  66. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  67. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  68. package/dist/scripts/describe-workspace-apps.js +5 -4
  69. package/dist/server/action-change-marker-write.d.ts +1 -0
  70. package/dist/server/action-change-marker-write.js +6 -1
  71. package/dist/server/action-routes.js +3 -1
  72. package/dist/server/agent-chat/prompt-resources.d.ts +1 -1
  73. package/dist/server/agent-chat/prompt-resources.js +3 -3
  74. package/dist/server/agent-chat-plugin.js +4 -5
  75. package/dist/server/agent-context-agents.d.ts +17 -0
  76. package/dist/server/agent-context-agents.js +40 -0
  77. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  78. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  79. package/dist/server/capture-error.js +3 -1
  80. package/dist/server/complete-text.js +7 -1
  81. package/dist/server/filesystem-action-entry.js +3 -0
  82. package/dist/server/index.d.ts +2 -1
  83. package/dist/server/index.js +2 -1
  84. package/dist/shared/chat-first-app-creation.js +1 -1
  85. package/dist/shared/error-noise.d.ts +1 -1
  86. package/dist/shared/error-noise.js +12 -0
  87. package/dist/shared/targeted-text-edit.d.ts +13 -7
  88. package/dist/shared/targeted-text-edit.js +22 -10
  89. package/dist/shared/workspace-default-app.d.ts +4 -0
  90. package/dist/shared/workspace-default-app.js +16 -0
  91. package/dist/sharing/access.d.ts +14 -0
  92. package/dist/sharing/access.js +131 -42
  93. package/dist/sharing/index.d.ts +1 -1
  94. package/dist/sharing/index.js +1 -1
  95. package/dist/sharing/registry.d.ts +11 -0
  96. package/dist/templates/builder-code-starter/.agents/skills/multi-app-workspace/SKILL.md +1 -1
  97. package/dist/templates/chat/.agents/skills/actions/SKILL.md +11 -1
  98. package/dist/templates/chat/.agents/skills/sharing/SKILL.md +4 -0
  99. package/dist/templates/chat/DEVELOPING.md +1 -0
  100. package/dist/templates/chat/server/routes/index.get.ts +1 -0
  101. package/dist/templates/default/.agents/skills/actions/SKILL.md +11 -1
  102. package/dist/templates/default/.agents/skills/sharing/SKILL.md +4 -0
  103. package/dist/templates/default/DEVELOPING.md +1 -1
  104. package/dist/templates/default/server/routes/index.get.ts +1 -0
  105. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +6 -8
  106. package/dist/templates/headless/.agents/skills/actions/SKILL.md +11 -1
  107. package/dist/templates/headless/.agents/skills/sharing/SKILL.md +4 -0
  108. package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +11 -1
  109. package/dist/templates/workspace-core/.agents/skills/adding-workspace-apps/SKILL.md +5 -5
  110. package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +4 -0
  111. package/dist/templates/workspace-root/AGENTS.md +3 -2
  112. package/dist/templates/workspace-root/README.md +4 -4
  113. package/dist/tracking/providers.js +5 -2
  114. package/dist/tracking/registry.js +11 -0
  115. package/dist/triggers/condition-evaluator.js +3 -1
  116. package/docs/content/cloneable-saas.mdx +2 -2
  117. package/docs/content/dispatch.mdx +1 -1
  118. package/docs/content/multi-app-workspace.mdx +8 -2
  119. package/docs/content/server-routes.mdx +7 -0
  120. package/docs/content/template-dispatch-reference.mdx +6 -5
  121. package/package.json +4 -4
  122. package/src/templates/builder-code-starter/.agents/skills/multi-app-workspace/SKILL.md +1 -1
  123. package/src/templates/chat/.agents/skills/actions/SKILL.md +11 -1
  124. package/src/templates/chat/.agents/skills/sharing/SKILL.md +4 -0
  125. package/src/templates/chat/DEVELOPING.md +1 -0
  126. package/src/templates/chat/server/routes/index.get.ts +1 -0
  127. package/src/templates/default/.agents/skills/actions/SKILL.md +11 -1
  128. package/src/templates/default/.agents/skills/sharing/SKILL.md +4 -0
  129. package/src/templates/default/DEVELOPING.md +1 -1
  130. package/src/templates/default/server/routes/index.get.ts +1 -0
  131. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +6 -8
  132. package/src/templates/headless/.agents/skills/actions/SKILL.md +11 -1
  133. package/src/templates/headless/.agents/skills/sharing/SKILL.md +4 -0
  134. package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +11 -1
  135. package/src/templates/workspace-core/.agents/skills/adding-workspace-apps/SKILL.md +5 -5
  136. package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +4 -0
  137. package/src/templates/workspace-root/AGENTS.md +3 -2
  138. package/src/templates/workspace-root/README.md +4 -4
  139. package/dist/templates/builder-code-starter/server/routes/index.get.ts +0 -2
  140. package/src/templates/builder-code-starter/server/routes/index.get.ts +0 -2
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * The execution frame each action invocation runs in, and the evidence a
3
3
  * dispatcher vouched for that one invocation. Not in the package exports: the
4
- * action entry exposes no frame-evidence helpers, and request input or
4
+ * action entry exposes no frame-evidence helpers but `noteExternalWrite`, which
5
+ * can only add observed-write evidence, and `withoutWriteObservation`, which
6
+ * can only keep a write from being counted. Request input or
5
7
  * `ActionRunContext` fields never become evidence. Trusted server dispatcher
6
8
  * APIs (`executeAgentToolCall`, `runAgentLoop`) can still supply approval and
7
9
  * attribution. Nothing here imports `action.ts` at runtime, so `action.ts` and
@@ -36,6 +38,10 @@ export type ActionExecutionFrame = {
36
38
  writeEntered: boolean;
37
39
  /** One of those writes publishes action changes. */
38
40
  changeEntered: boolean;
41
+ /** This call, or one nested in it, was seen to write. */
42
+ writeObserved: boolean;
43
+ /** Which channel saw it first, for diagnostics. */
44
+ writeObservedChannel?: string;
39
45
  /** This call's classification, once validated arguments set it. */
40
46
  writes?: boolean;
41
47
  emitsChange?: boolean;
@@ -44,6 +50,8 @@ export type ActionExecutionFrame = {
44
50
  record?: ActionCallRecord;
45
51
  context?: ActionRunContext;
46
52
  nestedWriteEntered?: boolean;
53
+ /** The write was seen in a call nested in this one, not this call's own. */
54
+ nestedWriteObserved?: boolean;
47
55
  raised?: unknown;
48
56
  /** The dispatcher's outcome, which takes the record as it is made. */
49
57
  outcome?: ActionExecutionOutcome;
@@ -71,3 +79,29 @@ export declare function currentInvocationEvidence(): ActionInvocationEvidence |
71
79
  * another call is recorded here.
72
80
  */
73
81
  export declare function runOutsideActionInvocation<T>(fn: () => T): T;
82
+ /**
83
+ * Whether a write seen now would add evidence: not outside a call, not inside
84
+ * bookkeeping, and not once the call has already seen one. Lets a caller skip
85
+ * work it would only do to classify a write.
86
+ */
87
+ export declare function observingWrites(): boolean;
88
+ /**
89
+ * Record that the running call, and every call it runs inside, was seen to
90
+ * write through `channel`. Call it before the write is sent: a write still in
91
+ * flight when its failure arrives must already count. Evidence is only added,
92
+ * never cleared.
93
+ */
94
+ export declare function noteObservedWrite(channel: string): void;
95
+ /**
96
+ * Run the framework's own bookkeeping, such as an audit row, a change marker or
97
+ * an analytics event, without it counting as a write of the call it runs
98
+ * inside. Scoped to that call's frame, so an action it runs still records its
99
+ * own writes. Use it only for bookkeeping, or for a write a retry provably
100
+ * cannot repeat, such as one keyed by a deterministic id or a dedup key. Never
101
+ * wrap a write the call exists to make: a failure after it would read as
102
+ * having saved nothing, and the agent would repeat it. The callback must await
103
+ * the work inside itself: a Drizzle query builder is a thenable that runs when
104
+ * awaited, after the callback returned, so `() => db.insert(t).values(v)`
105
+ * exempts nothing.
106
+ */
107
+ export declare function withoutWriteObservation<T>(fn: () => T): T;
@@ -8,6 +8,10 @@ const ActionExecutionStorage = getAsyncLocalStorageCtor();
8
8
  export const actionExecutionStorage = ActionExecutionStorage
9
9
  ? new ActionExecutionStorage()
10
10
  : undefined;
11
+ /** The frame whose writes are not observed, for the work that runs inside it. */
12
+ const unobservedFrame = ActionExecutionStorage
13
+ ? new ActionExecutionStorage()
14
+ : undefined;
11
15
  /** Evidence vouched for a run, until that run opens its frame. */
12
16
  const vouched = new WeakMap();
13
17
  /** Each frame's evidence, until its invocation settles. */
@@ -53,3 +57,49 @@ export function runOutsideActionInvocation(fn) {
53
57
  return fn();
54
58
  return actionExecutionStorage.run({ failures: state.failures }, fn);
55
59
  }
60
+ /**
61
+ * Whether a write seen now would add evidence: not outside a call, not inside
62
+ * bookkeeping, and not once the call has already seen one. Lets a caller skip
63
+ * work it would only do to classify a write.
64
+ */
65
+ export function observingWrites() {
66
+ const frame = actionExecutionStorage?.getStore()?.frame;
67
+ return (frame !== undefined &&
68
+ !frame.writeObserved &&
69
+ unobservedFrame?.getStore() !== frame);
70
+ }
71
+ /**
72
+ * Record that the running call, and every call it runs inside, was seen to
73
+ * write through `channel`. Call it before the write is sent: a write still in
74
+ * flight when its failure arrives must already count. Evidence is only added,
75
+ * never cleared.
76
+ */
77
+ export function noteObservedWrite(channel) {
78
+ const frame = actionExecutionStorage?.getStore()?.frame;
79
+ if (!frame || unobservedFrame?.getStore() === frame)
80
+ return;
81
+ for (let f = frame; f; f = f.parent) {
82
+ f.writeObserved = true;
83
+ f.writeObservedChannel ??= channel;
84
+ if (f !== frame)
85
+ f.nestedWriteObserved = true;
86
+ }
87
+ }
88
+ /**
89
+ * Run the framework's own bookkeeping, such as an audit row, a change marker or
90
+ * an analytics event, without it counting as a write of the call it runs
91
+ * inside. Scoped to that call's frame, so an action it runs still records its
92
+ * own writes. Use it only for bookkeeping, or for a write a retry provably
93
+ * cannot repeat, such as one keyed by a deterministic id or a dedup key. Never
94
+ * wrap a write the call exists to make: a failure after it would read as
95
+ * having saved nothing, and the agent would repeat it. The callback must await
96
+ * the work inside itself: a Drizzle query builder is a thenable that runs when
97
+ * awaited, after the callback returned, so `() => db.insert(t).values(v)`
98
+ * exempts nothing.
99
+ */
100
+ export function withoutWriteObservation(fn) {
101
+ const frame = actionExecutionStorage?.getStore()?.frame;
102
+ if (!frame || !unobservedFrame)
103
+ return fn();
104
+ return unobservedFrame.run(frame, fn);
105
+ }
package/dist/action.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { StandardSchemaV1 } from "@standard-schema/spec";
2
2
  import { actionCallReadOnly, type ActionCallClassification } from "./action-call-classification.js";
3
- import { ACTION_EXECUTION_KEY, type ActionExecutionRun } from "./action-frame.js";
3
+ import { ACTION_EXECUTION_KEY, withoutWriteObservation, type ActionExecutionRun } from "./action-frame.js";
4
4
  import { type ActionMcpOutputContract } from "./action-output-contract.js";
5
5
  import { type ActionChatUIConfig } from "./action-ui.js";
6
6
  import type { ActionTool, AgentChatAttachment, AgentChatEvent } from "./agent/types.js";
@@ -9,7 +9,7 @@ import { type ActionAccessConfig } from "./authorization/action-access-runtime.j
9
9
  import { type JsonValueWriter } from "./json-schema/json-value.js";
10
10
  export { ACTION_RECEIPT_SCHEMA_VERSION, DOMAIN_EVENT_CONTENT_TYPE, DOMAIN_EVENT_SPEC_VERSION, ActionReceiptValidationError, assertActionDomainEvent, assertActionReceipt, commitAction, createActionDomainEvent, parseActionDomainEvent, parseActionReceipt, serializeActionDomainEvent, serializeActionReceipt, } from "./action-receipt.js";
11
11
  export type { ActionAtomicAdapter, ActionAtomicContext, ActionAtomicOutcome, ActionDomainEvent, ActionDomainEventDraft, ActionDomainEventReference, ActionJsonPrimitive, ActionJsonValue, ActionReceipt, ActionReceiptProvenance, CommitActionOptions, } from "./action-receipt.js";
12
- export { ACTION_EXECUTION_KEY };
12
+ export { ACTION_EXECUTION_KEY, withoutWriteObservation };
13
13
  /**
14
14
  * Records a call refused before its run, for arguments its caller found
15
15
  * invalid, as the action's own validation failure would be recorded: through
@@ -139,13 +139,14 @@ export interface ActionErrors<TCode extends string> {
139
139
  }
140
140
  export declare function defineActionErrors<const TCode extends string>(...codes: [TCode, ...TCode[]]): ActionErrors<TCode>;
141
141
  /**
142
- * A rejection raised before the action's first write: a missing target, an
143
- * edit that matches nothing, a patch identical to what is saved. A caller may
144
- * then retry with corrected arguments instead of treating the call as
145
- * possibly applied. The framework accepts the claim only for the call that
146
- * raised it, and only when no write action nested in that call entered its
147
- * handler; otherwise it reads as any other failure. It cannot see the
148
- * handler's own database or provider writes, so throw it before the first.
142
+ * A rejection raised before the action saved anything that matters, so a
143
+ * caller can retry with corrected arguments. The framework reads a failure
144
+ * from the writes it saw, so most rejections need no label. This one is an
145
+ * override that wins over an observed write, for example when the only thing
146
+ * written first is something a retry harmlessly repeats, such as uploads
147
+ * nobody has referenced yet. The framework accepts the claim only for the call
148
+ * that raised it, and only when no write action nested in that call entered
149
+ * its handler; otherwise it reads as any other failure.
149
150
  */
150
151
  export declare class NoWriteError extends ActionContractError {
151
152
  readonly noWrite = true;
@@ -187,6 +188,15 @@ export declare function isActionOutputContractError(error: unknown): error is Ac
187
188
  * the call changed nothing.
188
189
  */
189
190
  export declare function actionFailureEffect(error: unknown): ActionOutputEffect | undefined;
191
+ /**
192
+ * Record that the running action call wrote somewhere the framework cannot
193
+ * watch, such as a socket, a file or a vendor SDK that does not use `fetch`, so
194
+ * that a later failure of the call is not read as one that saved nothing. Call
195
+ * it before the write is sent. It only adds evidence: nothing it records can be
196
+ * cleared, and outside an action call it does nothing. Database writes and
197
+ * `fetch` requests that are not GET, HEAD or OPTIONS need no call.
198
+ */
199
+ export declare function noteExternalWrite(channel: string): void;
190
200
  /** What a dispatched call did, and whether other sessions must refresh. */
191
201
  export interface InvocationEffect {
192
202
  /** `unknown`: a write may have run that nothing confirms or rules out. */
@@ -204,6 +214,15 @@ export interface InvocationEffect {
204
214
  * declaration, which only then a receipt, `retrySafe` or a typed failure the
205
215
  * call raised itself may narrow. A refused call is settled before its error is
206
216
  * read.
217
+ *
218
+ * A recorded call that threw by itself needs a write the framework saw it
219
+ * send as well, not just a write handler having started: a rejection raised
220
+ * before anything was saved is `none`. Seeing a write only narrows what
221
+ * entered, so a call declared read-only that writes anyway is no more suspect
222
+ * than before. A call whose result never came may still be running and write
223
+ * later, so observing nothing proves nothing and it keeps the entered
224
+ * evidence alone. So does `changed`: a refresh nothing needed costs less than
225
+ * a missed one.
207
226
  */
208
227
  export declare function invocationEffect(entry: {
209
228
  changeEvents?: boolean;
@@ -470,6 +489,12 @@ interface DefineActionWithSchema<TSchema extends StandardSchemaV1, TReturn = any
470
489
  mcpAnnotations?: ActionMcpToolAnnotations;
471
490
  deferLoading?: boolean;
472
491
  eagerInputStreaming?: boolean;
492
+ /** An empty list on this action sets an option and erases nothing saved, so
493
+ * a call that pads unused fields with `null` and `[]` can run as sent. Leave
494
+ * this off for any action that stores a list: there, `[]` beside a padded
495
+ * `null` might be a real clear or padding, so the call is refused rather
496
+ * than guessed at. */
497
+ emptyListsEraseNothing?: boolean;
473
498
  readOnly?: boolean;
474
499
  grounding?: boolean;
475
500
  allowInPlanMode?: boolean;
@@ -576,6 +601,7 @@ interface DefineActionWithParams<TParams extends Record<string, ParameterSchema>
576
601
  mcpAnnotations?: ActionMcpToolAnnotations;
577
602
  deferLoading?: boolean;
578
603
  eagerInputStreaming?: boolean;
604
+ emptyListsEraseNothing?: boolean;
579
605
  readOnly?: boolean;
580
606
  grounding?: boolean;
581
607
  allowInPlanMode?: boolean;
@@ -618,6 +644,7 @@ export interface ActionDefinition<TInput, TReturn> {
618
644
  readonly mcpAnnotations?: ActionMcpToolAnnotations;
619
645
  readonly deferLoading?: boolean;
620
646
  readonly eagerInputStreaming?: boolean;
647
+ readonly emptyListsEraseNothing?: boolean;
621
648
  readonly readOnly?: boolean;
622
649
  readonly grounding?: boolean;
623
650
  readonly allowInPlanMode?: boolean;
@@ -681,6 +708,10 @@ export interface ActionCallEvidence {
681
708
  readonly changeEntered: boolean;
682
709
  /** One of those writes was nested in the call, not the call's own. */
683
710
  readonly nestedWriteEntered?: boolean;
711
+ /** The framework saw the call, or one nested in it, send a write. */
712
+ readonly writeObserved: boolean;
713
+ /** One of those observed writes was nested in the call, not the call's own. */
714
+ readonly nestedWriteObserved?: boolean;
684
715
  /**
685
716
  * The failure this call raised or rebuilt itself; only its typed effect
686
717
  * speaks for the call. Its own, so another call throwing it again cannot
package/dist/action.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { copyActionArguments, snapshotActionArguments, } from "./action-arguments-identity.js";
2
2
  import { actionCallIsRetrySafe, actionCallReadOnly, actionOutputEffect, classifyActionCall, } from "./action-call-classification.js";
3
- import { ACTION_EXECUTION_KEY, actionExecutionStorage, endInvocationEvidence, holdInvocationEvidence, runOutsideActionInvocation, takeVouchedEvidence, } from "./action-frame.js";
3
+ import { ACTION_EXECUTION_KEY, actionExecutionStorage, endInvocationEvidence, holdInvocationEvidence, noteObservedWrite, runOutsideActionInvocation, takeVouchedEvidence, withoutWriteObservation, } from "./action-frame.js";
4
4
  import { coerceGatewayStringifiedArgs } from "./action-input-coercion.js";
5
5
  import { buildActionMcpOutputContract, describeOutputContractIssue, inputSchemaToJsonSchema, maskedIssuePath, outputSchemaToJsonSchema, } from "./action-output-contract.js";
6
6
  import { normalizeActionChatUIConfig, } from "./action-ui.js";
7
+ import { normalizeOptionalToolPlaceholders } from "./agent/optional-placeholders.js";
8
+ import { getStrictToolInputValidator } from "./agent/strict-tool-input.js";
7
9
  import { normalizeAuditConfig, resolveAuditAttach } from "./audit/config.js";
8
10
  import { assertRegisteredActionAccess, } from "./authorization/action-access-runtime.js";
9
11
  import { writeBoundedJsonValue, } from "./json-schema/json-value.js";
@@ -12,9 +14,10 @@ import { jsonTypeOf, plainJsonSnapshot, recordInputSchemaVerdict, UninterpretedS
12
14
  import { assertUsableSchema } from "./json-schema/usable-schema.js";
13
15
  import { describeThrownForLog } from "./mcp/embed-redaction.js";
14
16
  import { parseServiceIdentityEmail } from "./org/service-identity.js";
17
+ import { observeOutboundWrites } from "./outbound-write-observation.js";
15
18
  import { wrapRunWithActionTracking } from "./tracking/action-lifecycle.js";
16
19
  export { ACTION_RECEIPT_SCHEMA_VERSION, DOMAIN_EVENT_CONTENT_TYPE, DOMAIN_EVENT_SPEC_VERSION, ActionReceiptValidationError, assertActionDomainEvent, assertActionReceipt, commitAction, createActionDomainEvent, parseActionDomainEvent, parseActionReceipt, serializeActionDomainEvent, serializeActionReceipt, } from "./action-receipt.js";
17
- export { ACTION_EXECUTION_KEY };
20
+ export { ACTION_EXECUTION_KEY, withoutWriteObservation };
18
21
  /**
19
22
  * An action's recorder for a call refused before its run, kept on its
20
23
  * execution key so every copy of the entry that carries the key keeps it, even
@@ -102,13 +105,14 @@ export function defineActionErrors(...codes) {
102
105
  return Object.freeze(errors);
103
106
  }
104
107
  /**
105
- * A rejection raised before the action's first write: a missing target, an
106
- * edit that matches nothing, a patch identical to what is saved. A caller may
107
- * then retry with corrected arguments instead of treating the call as
108
- * possibly applied. The framework accepts the claim only for the call that
109
- * raised it, and only when no write action nested in that call entered its
110
- * handler; otherwise it reads as any other failure. It cannot see the
111
- * handler's own database or provider writes, so throw it before the first.
108
+ * A rejection raised before the action saved anything that matters, so a
109
+ * caller can retry with corrected arguments. The framework reads a failure
110
+ * from the writes it saw, so most rejections need no label. This one is an
111
+ * override that wins over an observed write, for example when the only thing
112
+ * written first is something a retry harmlessly repeats, such as uploads
113
+ * nobody has referenced yet. The framework accepts the claim only for the call
114
+ * that raised it, and only when no write action nested in that call entered
115
+ * its handler; otherwise it reads as any other failure.
112
116
  */
113
117
  export class NoWriteError extends ActionContractError {
114
118
  noWrite = true;
@@ -209,6 +213,25 @@ function readableFailure(error) {
209
213
  return Object.assign(new Error("The call failed, and its error could not be read."), { errorCode: "error_unreadable" });
210
214
  }
211
215
  }
216
+ /**
217
+ * Record that the running action call wrote somewhere the framework cannot
218
+ * watch, such as a socket, a file or a vendor SDK that does not use `fetch`, so
219
+ * that a later failure of the call is not read as one that saved nothing. Call
220
+ * it before the write is sent. It only adds evidence: nothing it records can be
221
+ * cleared, and outside an action call it does nothing. Database writes and
222
+ * `fetch` requests that are not GET, HEAD or OPTIONS need no call.
223
+ */
224
+ export function noteExternalWrite(channel) {
225
+ noteObservedWrite(channel);
226
+ }
227
+ /**
228
+ * A `fetch` that cannot be wrapped hides every HTTP write, so a call that
229
+ * cannot be watched counts as having sent one and its failure stays `unknown`.
230
+ */
231
+ function noteUnwatchedFetch(status) {
232
+ if (status === "fetch-unwritable")
233
+ noteObservedWrite("http-unwatched");
234
+ }
212
235
  /**
213
236
  * What a dispatched call did, for every transport, from what ran in it by the
214
237
  * time it settled or was abandoned. In order: an action's entry that recorded
@@ -219,6 +242,15 @@ function readableFailure(error) {
219
242
  * declaration, which only then a receipt, `retrySafe` or a typed failure the
220
243
  * call raised itself may narrow. A refused call is settled before its error is
221
244
  * read.
245
+ *
246
+ * A recorded call that threw by itself needs a write the framework saw it
247
+ * send as well, not just a write handler having started: a rejection raised
248
+ * before anything was saved is `none`. Seeing a write only narrows what
249
+ * entered, so a call declared read-only that writes anyway is no more suspect
250
+ * than before. A call whose result never came may still be running and write
251
+ * later, so observing nothing proves nothing and it keeps the entered
252
+ * evidence alone. So does `changed`: a refresh nothing needed costs less than
253
+ * a missed one.
222
254
  */
223
255
  export function invocationEffect(entry, outcome, settled) {
224
256
  const { evidence, record } = outcome;
@@ -236,16 +268,24 @@ export function invocationEffect(entry, outcome, settled) {
236
268
  const typed = failed && !failed.lost && (!evidence || evidence.raised === failed.error)
237
269
  ? actionFailureEffect(failed.error)
238
270
  : undefined;
239
- if (evidence?.writeEntered && (evidence.nestedWriteEntered || !record)) {
271
+ const enteredNested = evidence?.writeEntered && (evidence.nestedWriteEntered || !record)
272
+ ? evidence
273
+ : undefined;
274
+ const enteredOwn = record ? record.evidence.writeEntered : readOnly !== true;
275
+ const changed = enteredNested
276
+ ? enteredNested.changeEntered
277
+ : record
278
+ ? record.evidence.changeEntered && !(failed && typed === "none")
279
+ : entry.changeEvents !== false &&
280
+ (failed ? typed === "committed" : enteredOwn);
281
+ const observed = record !== undefined && failed !== undefined && !failed.lost;
282
+ if (enteredNested && (!observed || enteredNested.nestedWriteObserved)) {
240
283
  return {
241
284
  effect: !failed || typed === "committed" ? "committed" : "unknown",
242
- changed: evidence.changeEntered,
285
+ changed,
243
286
  };
244
287
  }
245
- const wrote = record ? record.evidence.writeEntered : readOnly !== true;
246
- const changed = record
247
- ? record.evidence.changeEntered && !(failed && typed === "none")
248
- : entry.changeEvents !== false && (failed ? typed === "committed" : wrote);
288
+ const wrote = enteredOwn && (!observed || record.evidence.writeObserved);
249
289
  const effect = !failed
250
290
  ? (settled.receipt?.changed ?? wrote)
251
291
  ? "committed"
@@ -746,6 +786,9 @@ export function defineAction(options) {
746
786
  ...(typeof options.eagerInputStreaming === "boolean"
747
787
  ? { eagerInputStreaming: options.eagerInputStreaming }
748
788
  : {}),
789
+ ...(typeof options.emptyListsEraseNothing === "boolean"
790
+ ? { emptyListsEraseNothing: options.emptyListsEraseNothing }
791
+ : {}),
749
792
  ...(typeof readOnly === "boolean" ? { readOnly } : {}),
750
793
  ...(typeof options.grounding === "boolean"
751
794
  ? { grounding: options.grounding }
@@ -1429,6 +1472,7 @@ function wrapRunWithExecutionBoundary(run, key) {
1429
1472
  const boundaryRun = async (args, ctx) => {
1430
1473
  if (!actionExecutionStorage)
1431
1474
  return run(args, ctx);
1475
+ const fetchStatus = observeOutboundWrites();
1432
1476
  const parent = actionExecutionStorage.getStore();
1433
1477
  const frame = parent?.frame?.boundaryPending &&
1434
1478
  (parent.frame.run === boundaryRun || parent.frame.key === key)
@@ -1440,6 +1484,7 @@ function wrapRunWithExecutionBoundary(run, key) {
1440
1484
  handlerEntered: false,
1441
1485
  writeEntered: false,
1442
1486
  changeEntered: false,
1487
+ writeObserved: false,
1443
1488
  };
1444
1489
  frame.key = key;
1445
1490
  frame.boundaryPending = false;
@@ -1449,6 +1494,7 @@ function wrapRunWithExecutionBoundary(run, key) {
1449
1494
  failures: parent?.failures ?? new WeakMap(),
1450
1495
  };
1451
1496
  return actionExecutionStorage.run(state, async () => {
1497
+ noteUnwatchedFetch(fetchStatus);
1452
1498
  try {
1453
1499
  return await run(args, ctx);
1454
1500
  }
@@ -1473,6 +1519,7 @@ export async function runActionWithExecutionOutcome(run, args, ctx, outcome, ent
1473
1519
  // so without one no row records the evidence vouched for this one.
1474
1520
  if (!actionExecutionStorage)
1475
1521
  return run(args, ctx);
1522
+ const fetchStatus = observeOutboundWrites();
1476
1523
  const parent = actionExecutionStorage.getStore();
1477
1524
  const entryKey = entry
1478
1525
  ? entry[ACTION_EXECUTION_KEY]
@@ -1487,6 +1534,7 @@ export async function runActionWithExecutionOutcome(run, args, ctx, outcome, ent
1487
1534
  handlerEntered: true,
1488
1535
  writeEntered: false,
1489
1536
  changeEntered: false,
1537
+ writeObserved: false,
1490
1538
  boundaryPending: true,
1491
1539
  outcome,
1492
1540
  };
@@ -1500,6 +1548,7 @@ export async function runActionWithExecutionOutcome(run, args, ctx, outcome, ent
1500
1548
  failures: parent?.failures ?? new WeakMap(),
1501
1549
  };
1502
1550
  return actionExecutionStorage.run(state, async () => {
1551
+ noteUnwatchedFetch(fetchStatus);
1503
1552
  try {
1504
1553
  return await run(args, ctx);
1505
1554
  }
@@ -1620,8 +1669,14 @@ export async function actionCallIntent(entry, input) {
1620
1669
  : undefined;
1621
1670
  let snapshot;
1622
1671
  try {
1672
+ const repaired = getStrictToolInputValidator(entry)
1673
+ ? { input, error: undefined }
1674
+ : normalizeOptionalToolPlaceholders(entry.tool?.parameters, coerceGatewayStringifiedArgs(input, entry.tool?.parameters), entry, { nullOnly: true });
1675
+ if (repaired.error)
1676
+ throw repaired.error;
1677
+ args = repaired.input;
1623
1678
  if (schema) {
1624
- args = await validateActionArgs(schema, input, entry.tool?.parameters);
1679
+ args = await validateActionArgs(schema, args, entry.tool?.parameters);
1625
1680
  }
1626
1681
  // Arguments with no exact snapshot (an accessor, a Map, a class
1627
1682
  // instance) are refused: a gate could not be sure it read what runs.
@@ -1,4 +1,5 @@
1
1
  import { execFile } from "node:child_process";
2
+ import { noteObservedWrite, withoutWriteObservation, } from "../../action-frame.js";
2
3
  export class ShellCliAdapter {
3
4
  name;
4
5
  description;
@@ -16,7 +17,7 @@ export class ShellCliAdapter {
16
17
  }
17
18
  async isAvailable() {
18
19
  try {
19
- const result = await this.execute(["--version"]);
20
+ const result = await withoutWriteObservation(() => this.execute(["--version"]));
20
21
  return result.exitCode === 0;
21
22
  }
22
23
  catch {
@@ -24,6 +25,8 @@ export class ShellCliAdapter {
24
25
  }
25
26
  }
26
27
  execute(args) {
28
+ // The command is the caller's, so nothing says it only reads.
29
+ noteObservedWrite("cli");
27
30
  return new Promise((resolve) => {
28
31
  const child = execFile(this.command, args, {
29
32
  env: this.env ? { ...process.env, ...this.env } : process.env,
@@ -12,6 +12,7 @@ import { getBuilderGatewayRequestHeaders } from "./builder-gateway-headers.js";
12
12
  import { gatewayVisitorFacingError, LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "./credential-errors.js";
13
13
  import { BUILDER_GATEWAY_INTERNAL_ERROR_CODE, classifyTerminalErrorCode, canonicalizeBuilderGatewayErrorCode, describeErrorWithCauses, extractRetryAfterMs, isBareProviderRejectionMessage, isBuilderGatewayInternalErrorMessage, isContextOverflowCode, isContextOverflowMessage, isCreditsLimitErrorCode, isGenericInternalServerErrorMessage, isProviderConnectionErrorMessage, PROVIDER_TRANSIENT_REJECTION_ERROR_CODE, } from "./error-detail.js";
14
14
  import { FIRST_STREAM_EVENT_TIMEOUT_MS } from "./first-event-timeout.js";
15
+ import { inlineOwnedToolResultImages } from "./inline-tool-result-images.js";
15
16
  import { limitProviderTools } from "./limit-provider-tools.js";
16
17
  import { resolveMaxOutputTokensForEngine } from "./output-tokens.js";
17
18
  import { splitSystemPromptForCache, stablePrefixCacheControl, } from "./prompt-cache.js";
@@ -192,7 +193,7 @@ class BuilderEngine {
192
193
  : requestedModel;
193
194
  const toolNameMap = createProviderToolNameMap(opts.tools, opts.messages);
194
195
  const providerTools = limitProviderTools(opts.tools);
195
- const messages = engineMessagesToBuilderGatewayAnthropic(opts.messages, toolNameMap);
196
+ const messages = engineMessagesToBuilderGatewayAnthropic(await inlineOwnedToolResultImages(opts.messages), toolNameMap);
196
197
  const tools = engineToolsToAnthropic(providerTools, toolNameMap);
197
198
  const thinkingBudget = opts.providerOptions?.anthropic?.thinking?.budgetTokens;
198
199
  const reasoningEffort = normalizeReasoningEffortForModel(model, opts.reasoningEffort ??
@@ -0,0 +1,14 @@
1
+ import type { EngineMessage } from "./types.js";
2
+ /**
3
+ * Sends tool-result images we host (screenshots, exports) as bytes instead of
4
+ * URLs. The Builder gateway forwards a URL image unchanged: Vertex's Claude
5
+ * refuses it ("URL sources are not supported") and OpenAI fetches it itself,
6
+ * sometimes timing out. The stored tool result keeps its URL; only this
7
+ * request's copy carries bytes.
8
+ *
9
+ * Newest images are read first, within one request's hydration budget; URLs
10
+ * that are not ours do not spend it. An
11
+ * image that is not ours, or cannot be read or shrunk under the tool-result
12
+ * image limit, keeps its URL, which is what every request sent before.
13
+ */
14
+ export declare function inlineOwnedToolResultImages(messages: EngineMessage[]): Promise<EngineMessage[]>;
@@ -0,0 +1,165 @@
1
+ import { normalizeImageMediaType } from "../../file-upload/attachment-bytes.js";
2
+ import { claimOwnedAttachmentHydrationCandidate, createOwnedAttachmentHydrationBudget, hydrateOwnedImageUrl, releaseOwnedAttachmentHydrationCandidate, } from "../../file-upload/owned-attachment.js";
3
+ import { downscaleImageToFit } from "../../ingestion/media.js";
4
+ import { parseBase64DataUrl } from "../../shared/data-url.js";
5
+ import { MAX_TOOL_RESULT_IMAGE_BASE64_CHARS } from "../tool-result-images.js";
6
+ // A tool-result screenshot stays in the thread for every later model step, so
7
+ // the same URLs are requested again each step. Entries are bounded in count,
8
+ // encoded size, and age.
9
+ const INLINE_IMAGE_CACHE_TTL_MS = 5 * 60 * 1000;
10
+ const MAX_INLINE_IMAGE_CACHE_ENTRIES = 12;
11
+ const MAX_INLINE_IMAGE_CACHE_CHARS = 16_000_000;
12
+ const inlineImageCache = new Map();
13
+ let inlineImageCacheChars = 0;
14
+ function removeCachedInlineImage(url) {
15
+ const entry = inlineImageCache.get(url);
16
+ if (!entry)
17
+ return;
18
+ inlineImageCache.delete(url);
19
+ inlineImageCacheChars -= entry.image.data.length;
20
+ }
21
+ function getCachedInlineImage(url) {
22
+ const entry = inlineImageCache.get(url);
23
+ if (!entry)
24
+ return undefined;
25
+ if (entry.expiresAt <= Date.now()) {
26
+ removeCachedInlineImage(url);
27
+ return undefined;
28
+ }
29
+ // Re-insert so Map order tracks recency.
30
+ inlineImageCache.delete(url);
31
+ inlineImageCache.set(url, entry);
32
+ return entry.image;
33
+ }
34
+ function cacheInlineImage(url, image) {
35
+ removeCachedInlineImage(url);
36
+ while (inlineImageCache.size > 0 &&
37
+ (inlineImageCache.size >= MAX_INLINE_IMAGE_CACHE_ENTRIES ||
38
+ inlineImageCacheChars + image.data.length > MAX_INLINE_IMAGE_CACHE_CHARS)) {
39
+ removeCachedInlineImage(inlineImageCache.keys().next().value);
40
+ }
41
+ inlineImageCache.set(url, {
42
+ image,
43
+ expiresAt: Date.now() + INLINE_IMAGE_CACHE_TTL_MS,
44
+ });
45
+ inlineImageCacheChars += image.data.length;
46
+ }
47
+ /**
48
+ * Sends tool-result images we host (screenshots, exports) as bytes instead of
49
+ * URLs. The Builder gateway forwards a URL image unchanged: Vertex's Claude
50
+ * refuses it ("URL sources are not supported") and OpenAI fetches it itself,
51
+ * sometimes timing out. The stored tool result keeps its URL; only this
52
+ * request's copy carries bytes.
53
+ *
54
+ * Newest images are read first, within one request's hydration budget; URLs
55
+ * that are not ours do not spend it. An
56
+ * image that is not ours, or cannot be read or shrunk under the tool-result
57
+ * image limit, keeps its URL, which is what every request sent before.
58
+ */
59
+ export async function inlineOwnedToolResultImages(messages) {
60
+ const urls = [];
61
+ for (const message of [...messages].reverse()) {
62
+ for (const part of [...message.content].reverse()) {
63
+ if (part.type !== "tool-result" || part.isError || !part.images) {
64
+ continue;
65
+ }
66
+ for (const image of part.images) {
67
+ if (image.url && !image.data && !urls.includes(image.url)) {
68
+ urls.push(image.url);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ if (urls.length === 0)
74
+ return messages;
75
+ // Cache hits cost no hydration slot, so take them all before the budget can
76
+ // end the loop; otherwise an exhausted budget strands older cached images.
77
+ const inlined = new Map();
78
+ const uncached = [];
79
+ for (const url of urls) {
80
+ const cached = getCachedInlineImage(url);
81
+ if (cached)
82
+ inlined.set(url, cached);
83
+ else
84
+ uncached.push(url);
85
+ }
86
+ const budget = createOwnedAttachmentHydrationBudget();
87
+ for (const url of uncached) {
88
+ if (!claimOwnedAttachmentHydrationCandidate(budget))
89
+ break;
90
+ const image = await readOwnedImage(url, budget);
91
+ if (image) {
92
+ inlined.set(url, image);
93
+ cacheInlineImage(url, image);
94
+ }
95
+ }
96
+ if (inlined.size === 0)
97
+ return messages;
98
+ return messages.map((message) => ({
99
+ ...message,
100
+ content: message.content.map((part) => {
101
+ if (part.type !== "tool-result" ||
102
+ !part.images?.some((image) => image.url && inlined.has(image.url))) {
103
+ return part;
104
+ }
105
+ return {
106
+ ...part,
107
+ images: part.images.map((image) => {
108
+ const bytes = image.url ? inlined.get(image.url) : undefined;
109
+ if (!bytes)
110
+ return image;
111
+ return { ...bytes, ...(image.label ? { label: image.label } : {}) };
112
+ }),
113
+ };
114
+ }),
115
+ }));
116
+ }
117
+ async function readOwnedImage(url, budget) {
118
+ const result = await hydrateOwnedImageUrl(url, undefined, budget);
119
+ // Unowned and malformed URLs are rejected before any fetch, so they must not
120
+ // spend a slot that an older owned screenshot needs.
121
+ if (result.kind === "unowned") {
122
+ releaseOwnedAttachmentHydrationCandidate(budget);
123
+ return null;
124
+ }
125
+ if (result.kind === "failed") {
126
+ if (result.code === "invalid-url") {
127
+ releaseOwnedAttachmentHydrationCandidate(budget);
128
+ }
129
+ console.warn(`[builder-engine] tool-result image sent as a URL: ${result.code}`);
130
+ return null;
131
+ }
132
+ const parsed = parseBase64DataUrl(result.dataUrl);
133
+ let data = parsed?.data;
134
+ let mediaType = normalizeImageMediaType(result.mediaType);
135
+ if (data && mediaType && data.length > MAX_TOOL_RESULT_IMAGE_BASE64_CHARS) {
136
+ const resized = await shrinkToToolResultLimit(data);
137
+ data = resized?.data;
138
+ mediaType = resized ? normalizeImageMediaType(resized.mediaType) : null;
139
+ }
140
+ if (!data || !mediaType || data.length > MAX_TOOL_RESULT_IMAGE_BASE64_CHARS) {
141
+ console.warn("[builder-engine] tool-result image sent as a URL: image-too-large");
142
+ return null;
143
+ }
144
+ return { data, mediaType };
145
+ }
146
+ async function shrinkToToolResultLimit(base64) {
147
+ try {
148
+ // Budget in decoded bytes so the re-encoded base64 fits the limit.
149
+ const resized = await downscaleImageToFit({
150
+ data: Buffer.from(base64, "base64"),
151
+ maxBytes: Math.floor(MAX_TOOL_RESULT_IMAGE_BASE64_CHARS / 4) * 3,
152
+ });
153
+ if (!resized)
154
+ return null;
155
+ return {
156
+ data: Buffer.from(resized.data).toString("base64"),
157
+ mediaType: resized.mimeType,
158
+ };
159
+ }
160
+ catch (error) {
161
+ // coercion-ok: a failed shrink is reported by the caller, which keeps the URL.
162
+ console.warn("[builder-engine] tool-result image downscale failed:", error instanceof Error ? error.message : String(error));
163
+ return null;
164
+ }
165
+ }