@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.
- package/dist/action-frame.d.ts +35 -1
- package/dist/action-frame.js +50 -0
- package/dist/action.d.ts +40 -9
- package/dist/action.js +71 -16
- package/dist/adapters/cli/shell-adapter.js +4 -1
- package/dist/agent/engine/builder-engine.js +2 -1
- package/dist/agent/engine/inline-tool-result-images.d.ts +14 -0
- package/dist/agent/engine/inline-tool-result-images.js +165 -0
- package/dist/agent/optional-placeholders.d.ts +20 -0
- package/dist/agent/optional-placeholders.js +191 -0
- package/dist/agent/production-agent.d.ts +2 -0
- package/dist/agent/production-agent.js +15 -180
- package/dist/agent/types.d.ts +1 -0
- package/dist/application-state/handlers.d.ts +3 -3
- package/dist/audit/record.js +4 -1
- package/dist/cli/build-failure-report.d.ts +1 -0
- package/dist/cli/build-failure-report.js +8 -1
- package/dist/cli/create.d.ts +12 -8
- package/dist/cli/create.js +93 -94
- package/dist/cli/framework-package-versions.d.ts +10 -0
- package/dist/cli/framework-package-versions.js +53 -0
- package/dist/cli/git-http.d.ts +38 -0
- package/dist/cli/git-http.js +540 -0
- package/dist/cli/http-proxy.d.ts +3 -0
- package/dist/cli/http-proxy.js +21 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/sentry-telemetry.js +3 -0
- package/dist/cli/template-sync.js +2 -1
- package/dist/cli/workspace-dev.d.ts +1 -0
- package/dist/cli/workspace-dev.js +37 -3
- package/dist/client/use-db-sync.d.ts +14 -0
- package/dist/client/use-db-sync.js +100 -5
- package/dist/coding-tools/sandbox/local-child-process-adapter.js +2 -0
- package/dist/collab/agent-presence.js +8 -4
- package/dist/db/client.js +52 -2
- package/dist/db/create-get-db.js +52 -13
- package/dist/db/ddl-guard.js +7 -2
- package/dist/db/note-sql-statement.d.ts +5 -0
- package/dist/db/note-sql-statement.js +10 -0
- package/dist/db/sql-write-classifier.d.ts +9 -0
- package/dist/db/sql-write-classifier.js +165 -0
- package/dist/db/store-registry.js +23 -4
- package/dist/decisions/providers/jev.js +4 -2
- package/dist/decisions/providers/openai.js +4 -2
- package/dist/deploy/workspace-deploy.js +17 -8
- package/dist/embeddings/index.js +4 -2
- package/dist/file-upload/actions/upload-image.d.ts +3 -3
- package/dist/file-upload/owned-attachment.d.ts +2 -0
- package/dist/file-upload/owned-attachment.js +4 -0
- package/dist/framework-package-versions.json +17 -0
- package/dist/local-artifacts/index.js +8 -0
- package/dist/mcp/analytics.js +3 -3
- package/dist/mcp/direct-action-call.js +2 -2
- package/dist/mcp/embed-redaction.d.ts +8 -0
- package/dist/mcp/embed-redaction.js +21 -0
- package/dist/mcp/execute-action-call.js +2 -2
- package/dist/mcp/screen-memory-stdio.d.ts +3 -3
- package/dist/mcp-client/manager.js +2 -1
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/index.d.ts +1 -1
- package/dist/org/migrations.d.ts +1 -1
- package/dist/outbound-write-observation.d.ts +15 -0
- package/dist/outbound-write-observation.js +104 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/scripts/describe-workspace-apps.js +5 -4
- package/dist/server/action-change-marker-write.d.ts +1 -0
- package/dist/server/action-change-marker-write.js +6 -1
- package/dist/server/action-routes.js +3 -1
- package/dist/server/agent-chat/prompt-resources.d.ts +1 -1
- package/dist/server/agent-chat/prompt-resources.js +3 -3
- package/dist/server/agent-chat-plugin.js +4 -5
- package/dist/server/agent-context-agents.d.ts +17 -0
- package/dist/server/agent-context-agents.js +40 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/capture-error.js +3 -1
- package/dist/server/complete-text.js +7 -1
- package/dist/server/filesystem-action-entry.js +3 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -1
- package/dist/shared/chat-first-app-creation.js +1 -1
- package/dist/shared/error-noise.d.ts +1 -1
- package/dist/shared/error-noise.js +12 -0
- package/dist/shared/targeted-text-edit.d.ts +13 -7
- package/dist/shared/targeted-text-edit.js +22 -10
- package/dist/shared/workspace-default-app.d.ts +4 -0
- package/dist/shared/workspace-default-app.js +16 -0
- package/dist/sharing/access.d.ts +14 -0
- package/dist/sharing/access.js +131 -42
- package/dist/sharing/index.d.ts +1 -1
- package/dist/sharing/index.js +1 -1
- package/dist/sharing/registry.d.ts +11 -0
- package/dist/templates/builder-code-starter/.agents/skills/multi-app-workspace/SKILL.md +1 -1
- package/dist/templates/chat/.agents/skills/actions/SKILL.md +11 -1
- package/dist/templates/chat/.agents/skills/sharing/SKILL.md +4 -0
- package/dist/templates/chat/DEVELOPING.md +1 -0
- package/dist/templates/chat/server/routes/index.get.ts +1 -0
- package/dist/templates/default/.agents/skills/actions/SKILL.md +11 -1
- package/dist/templates/default/.agents/skills/sharing/SKILL.md +4 -0
- package/dist/templates/default/DEVELOPING.md +1 -1
- package/dist/templates/default/server/routes/index.get.ts +1 -0
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +6 -8
- package/dist/templates/headless/.agents/skills/actions/SKILL.md +11 -1
- package/dist/templates/headless/.agents/skills/sharing/SKILL.md +4 -0
- package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +11 -1
- package/dist/templates/workspace-core/.agents/skills/adding-workspace-apps/SKILL.md +5 -5
- package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +4 -0
- package/dist/templates/workspace-root/AGENTS.md +3 -2
- package/dist/templates/workspace-root/README.md +4 -4
- package/dist/tracking/providers.js +5 -2
- package/dist/tracking/registry.js +11 -0
- package/dist/triggers/condition-evaluator.js +3 -1
- package/docs/content/cloneable-saas.mdx +2 -2
- package/docs/content/dispatch.mdx +1 -1
- package/docs/content/multi-app-workspace.mdx +8 -2
- package/docs/content/server-routes.mdx +7 -0
- package/docs/content/template-dispatch-reference.mdx +6 -5
- package/package.json +4 -4
- package/src/templates/builder-code-starter/.agents/skills/multi-app-workspace/SKILL.md +1 -1
- package/src/templates/chat/.agents/skills/actions/SKILL.md +11 -1
- package/src/templates/chat/.agents/skills/sharing/SKILL.md +4 -0
- package/src/templates/chat/DEVELOPING.md +1 -0
- package/src/templates/chat/server/routes/index.get.ts +1 -0
- package/src/templates/default/.agents/skills/actions/SKILL.md +11 -1
- package/src/templates/default/.agents/skills/sharing/SKILL.md +4 -0
- package/src/templates/default/DEVELOPING.md +1 -1
- package/src/templates/default/server/routes/index.get.ts +1 -0
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +6 -8
- package/src/templates/headless/.agents/skills/actions/SKILL.md +11 -1
- package/src/templates/headless/.agents/skills/sharing/SKILL.md +4 -0
- package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +11 -1
- package/src/templates/workspace-core/.agents/skills/adding-workspace-apps/SKILL.md +5 -5
- package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +4 -0
- package/src/templates/workspace-root/AGENTS.md +3 -2
- package/src/templates/workspace-root/README.md +4 -4
- package/dist/templates/builder-code-starter/server/routes/index.get.ts +0 -2
- package/src/templates/builder-code-starter/server/routes/index.get.ts +0 -2
package/dist/action-frame.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/action-frame.js
CHANGED
|
@@ -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
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
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
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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
|
-
|
|
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
|
|
285
|
+
changed,
|
|
243
286
|
};
|
|
244
287
|
}
|
|
245
|
-
const wrote =
|
|
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,
|
|
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
|
+
}
|