@agent-native/core 0.0.0-beta-20260820115430 → 0.0.0-beta-20260820150521
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/corpus/templates/macros/netlify.toml +5 -0
- package/dist/action.js +20 -0
- package/dist/agent/production-agent.js +37 -10
- package/dist/agent/run-manager.d.ts +13 -2
- package/dist/agent/run-manager.js +61 -23
- package/dist/agent/types.d.ts +21 -0
- package/dist/catalog.json +1 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/framework-tools.d.ts +16 -1
- package/dist/framework-tools.js +15 -0
- package/dist/jobs/background-automation-runner.js +26 -2
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +6 -6
- package/dist/observability/traces.js +54 -11
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/action-discovery.d.ts +11 -4
- package/dist/server/action-discovery.js +11 -16
- package/dist/server/agent-chat/mcp-options.js +16 -1
- package/docs/content/external-agents-catalog.mdx +2 -0
- package/package.json +2 -2
|
@@ -12,3 +12,8 @@ NPM_CONFIG_PRODUCTION = "false"
|
|
|
12
12
|
|
|
13
13
|
[context.production.environment]
|
|
14
14
|
AGENT_NATIVE_HOSTED_HARNESS = "true"
|
|
15
|
+
# Macros owns its schema at release time (`migrate:production` above), so
|
|
16
|
+
# production request functions must not probe the database for migrations.
|
|
17
|
+
# Preview functions still run request-path migrations because previews do not
|
|
18
|
+
# run the production release migration step.
|
|
19
|
+
AGENT_NATIVE_RELEASE_MIGRATIONS = "1"
|
package/dist/action.js
CHANGED
|
@@ -415,6 +415,22 @@ const PROVIDER_SUPPORTED_FORMATS = new Set([
|
|
|
415
415
|
"ipv6",
|
|
416
416
|
"uuid",
|
|
417
417
|
]);
|
|
418
|
+
/**
|
|
419
|
+
* Anthropic's function-schema validator rejects regex lookaround -- `(?=`,
|
|
420
|
+
* `(?!`, `(?<=`, `(?<!` -- inside a `pattern`, with "Invalid JSON schema: regex
|
|
421
|
+
* lookaround is not supported", and it rejects the WHOLE request, before a
|
|
422
|
+
* token is generated. Interactive chat surfaces that as an error; a background
|
|
423
|
+
* run just dies with `http_400` in `agent_runs.error_detail` while the UI says
|
|
424
|
+
* nothing, which is how a scout can be down for days looking merely idle.
|
|
425
|
+
*
|
|
426
|
+
* Zod 4's `z.string().email()` compiles to two negative lookaheads, and it is
|
|
427
|
+
* written in ~35 action schemas across core, the templates, and the apps -- so
|
|
428
|
+
* this is answered here rather than by retyping every one of them, the same way
|
|
429
|
+
* typeless schemas and unsupported `format` values are. `pattern` is a hint for
|
|
430
|
+
* the model; the action's own zod schema still validates the value server-side,
|
|
431
|
+
* so dropping it widens nothing that was actually enforced.
|
|
432
|
+
*/
|
|
433
|
+
const LOOKAROUND_IN_PATTERN = /\(\?<?[=!]/;
|
|
418
434
|
/**
|
|
419
435
|
* Keywords that only ever CONSTRAIN a value and that OpenAI's validator rejects
|
|
420
436
|
* outright. Removing a constraint can never make a previously-valid document
|
|
@@ -440,6 +456,10 @@ export function stripUnsupportedSchemaKeywords(node) {
|
|
|
440
456
|
!PROVIDER_SUPPORTED_FORMATS.has(obj.format)) {
|
|
441
457
|
delete obj.format;
|
|
442
458
|
}
|
|
459
|
+
if (typeof obj.pattern === "string" &&
|
|
460
|
+
LOOKAROUND_IN_PATTERN.test(obj.pattern)) {
|
|
461
|
+
delete obj.pattern;
|
|
462
|
+
}
|
|
443
463
|
for (const key of SUBSCHEMA_VALUE_KEYS) {
|
|
444
464
|
// `items`/`additionalItems` may also be an array of subschemas.
|
|
445
465
|
const value = obj[key];
|
|
@@ -759,6 +759,14 @@ const VISIBLE_RETRY_THRESHOLD_MS = 10_000;
|
|
|
759
759
|
* < MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS (90s, above)
|
|
760
760
|
* < RUN_NO_PROGRESS_HARD_TIMEOUT_MS (150s, run-manager.ts)
|
|
761
761
|
*
|
|
762
|
+
* Ordering alone is NOT sufficient between the last two, because they do not
|
|
763
|
+
* measure the same events: the bounds above watch engine-stream frames, while
|
|
764
|
+
* the run-manager backstop watches events this loop FORWARDS. Extended
|
|
765
|
+
* thinking produces the first without the second, so the 150s bound sat inside
|
|
766
|
+
* the working distribution and killed live runs. What keeps them consistent is
|
|
767
|
+
* the `model_stream` start/end bracket around the engine call, which suspends
|
|
768
|
+
* the outer backstop while these inner bounds are the ones on duty.
|
|
769
|
+
*
|
|
762
770
|
* Background-function runs (proven 15-min budget, no ~40s wall) are likewise
|
|
763
771
|
* unaffected — they keep the full 90s window for every event, first or not.
|
|
764
772
|
*/
|
|
@@ -3464,6 +3472,27 @@ export async function runAgentLoop(opts) {
|
|
|
3464
3472
|
const activeToolInputs = new Map();
|
|
3465
3473
|
let zeroByteToolInputRestart;
|
|
3466
3474
|
let endedForNoProgress = false;
|
|
3475
|
+
// Bracket the engine call for the run manager's no-progress backstop
|
|
3476
|
+
// (`inFlightWorkDelta` in run-manager.ts). That backstop measures the
|
|
3477
|
+
// events this loop FORWARDS; extended thinking forwards none for
|
|
3478
|
+
// minutes while the frames arriving here prove the stream is alive, so
|
|
3479
|
+
// without the bracket a healthy generation reads as a wedged run and is
|
|
3480
|
+
// killed mid-stream. Closing is idempotent and MUST happen in a
|
|
3481
|
+
// `finally`: a leaked open suspends the backstop for the rest of the
|
|
3482
|
+
// run, which is worse than the stall it exists to catch.
|
|
3483
|
+
let modelStreamBracketOpen = false;
|
|
3484
|
+
const openModelStreamBracket = () => {
|
|
3485
|
+
if (modelStreamBracketOpen)
|
|
3486
|
+
return;
|
|
3487
|
+
modelStreamBracketOpen = true;
|
|
3488
|
+
send({ type: "model_stream", status: "start" });
|
|
3489
|
+
};
|
|
3490
|
+
const closeModelStreamBracket = () => {
|
|
3491
|
+
if (!modelStreamBracketOpen)
|
|
3492
|
+
return;
|
|
3493
|
+
modelStreamBracketOpen = false;
|
|
3494
|
+
send({ type: "model_stream", status: "end" });
|
|
3495
|
+
};
|
|
3467
3496
|
let lastModelStreamProgressAt = Date.now();
|
|
3468
3497
|
// FIX 2: true once a real (non-heartbeat) engine-stream event has been
|
|
3469
3498
|
// retrieved for THIS model call — gates
|
|
@@ -3540,6 +3569,10 @@ export async function runAgentLoop(opts) {
|
|
|
3540
3569
|
const checkpointNoProgress = () => {
|
|
3541
3570
|
if (endedForNoProgress)
|
|
3542
3571
|
return;
|
|
3572
|
+
// The stream is over as far as this loop is concerned, so end the
|
|
3573
|
+
// bracket before the boundary event rather than after it in the
|
|
3574
|
+
// `finally` — the checkpoint stays the last event of the chunk.
|
|
3575
|
+
closeModelStreamBracket();
|
|
3543
3576
|
send({
|
|
3544
3577
|
type: "auto_continue",
|
|
3545
3578
|
reason: "no_progress",
|
|
@@ -3637,6 +3670,7 @@ export async function runAgentLoop(opts) {
|
|
|
3637
3670
|
};
|
|
3638
3671
|
const eventIterator = eventStream[Symbol.asyncIterator]();
|
|
3639
3672
|
let eventIteratorDone = false;
|
|
3673
|
+
openModelStreamBracket();
|
|
3640
3674
|
try {
|
|
3641
3675
|
while (true) {
|
|
3642
3676
|
const nextEvent = await nextEngineEventWithNoProgressTimeout(eventIterator);
|
|
@@ -3700,11 +3734,7 @@ export async function runAgentLoop(opts) {
|
|
|
3700
3734
|
});
|
|
3701
3735
|
sendToolInputActivity(event.name, key, undefined, true);
|
|
3702
3736
|
if (noteZeroByteToolInputStart(event.name)) {
|
|
3703
|
-
|
|
3704
|
-
type: "auto_continue",
|
|
3705
|
-
reason: "no_progress",
|
|
3706
|
-
});
|
|
3707
|
-
endedForNoProgress = true;
|
|
3737
|
+
checkpointNoProgress();
|
|
3708
3738
|
break;
|
|
3709
3739
|
}
|
|
3710
3740
|
}
|
|
@@ -3745,11 +3775,7 @@ export async function runAgentLoop(opts) {
|
|
|
3745
3775
|
if (startedZeroByteInput &&
|
|
3746
3776
|
toolName &&
|
|
3747
3777
|
noteZeroByteToolInputStart(toolName)) {
|
|
3748
|
-
|
|
3749
|
-
type: "auto_continue",
|
|
3750
|
-
reason: "no_progress",
|
|
3751
|
-
});
|
|
3752
|
-
endedForNoProgress = true;
|
|
3778
|
+
checkpointNoProgress();
|
|
3753
3779
|
break;
|
|
3754
3780
|
}
|
|
3755
3781
|
}
|
|
@@ -3814,6 +3840,7 @@ export async function runAgentLoop(opts) {
|
|
|
3814
3840
|
}
|
|
3815
3841
|
}
|
|
3816
3842
|
finally {
|
|
3843
|
+
closeModelStreamBracket();
|
|
3817
3844
|
if (!eventIteratorDone) {
|
|
3818
3845
|
await requestEventIteratorReturn(eventIterator, !eventIteratorReturnRequested);
|
|
3819
3846
|
}
|
|
@@ -103,11 +103,22 @@ export declare const DEFAULT_BACKGROUND_RUN_SOFT_TIMEOUT_MS: number;
|
|
|
103
103
|
* with the client watching keepalives. This backstop covers every segment by
|
|
104
104
|
* construction: if no REAL progress event (see `shouldBumpProgressForEvent`;
|
|
105
105
|
* keepalives and zero-byte prep activity don't count) lands for this long —
|
|
106
|
-
* and no
|
|
107
|
-
*
|
|
106
|
+
* and no unit of work is in flight (see `inFlightWorkDelta`: tool calls,
|
|
107
|
+
* cross-app calls, and the model stream all legitimately emit nothing for
|
|
108
|
+
* minutes and each carry a bound of their own) — the run manager emits
|
|
108
109
|
* `auto_continue { reason: "no_progress" }` and aborts the chunk, exactly
|
|
109
110
|
* like the soft timeout, so the normal continuation machinery recovers it.
|
|
110
111
|
*
|
|
112
|
+
* Being numerically larger than the in-loop watchdogs is NOT what keeps this
|
|
113
|
+
* from killing a healthy run, and treating it that way is what made it do so:
|
|
114
|
+
* this clock and the loop's `lastModelStreamProgressAt` measure DIFFERENT
|
|
115
|
+
* events. An extended-thinking phase bumps the inner clock on every engine
|
|
116
|
+
* frame while forwarding nothing, so the inner watchdog correctly stayed quiet
|
|
117
|
+
* and this one saw pure silence — runs whose worst gap crossed 150s died while
|
|
118
|
+
* still streaming, some by a single second. Ordering between two clocks only
|
|
119
|
+
* means something when they watch the same events; suspending on in-flight
|
|
120
|
+
* work is what actually makes the two agree.
|
|
121
|
+
*
|
|
111
122
|
* This is now only the CEILING, not the value: `resolveRunNoProgressTimeoutMs`
|
|
112
123
|
* clamps the foreground backstop to a fraction of the chunk's soft timeout
|
|
113
124
|
* (~30s at a 40s chunk), which is BELOW the 90s in-loop watchdogs rather than
|
|
@@ -78,11 +78,22 @@ export const DEFAULT_BACKGROUND_RUN_SOFT_TIMEOUT_MS = BACKGROUND_SOFT_TIMEOUT_CE
|
|
|
78
78
|
* with the client watching keepalives. This backstop covers every segment by
|
|
79
79
|
* construction: if no REAL progress event (see `shouldBumpProgressForEvent`;
|
|
80
80
|
* keepalives and zero-byte prep activity don't count) lands for this long —
|
|
81
|
-
* and no
|
|
82
|
-
*
|
|
81
|
+
* and no unit of work is in flight (see `inFlightWorkDelta`: tool calls,
|
|
82
|
+
* cross-app calls, and the model stream all legitimately emit nothing for
|
|
83
|
+
* minutes and each carry a bound of their own) — the run manager emits
|
|
83
84
|
* `auto_continue { reason: "no_progress" }` and aborts the chunk, exactly
|
|
84
85
|
* like the soft timeout, so the normal continuation machinery recovers it.
|
|
85
86
|
*
|
|
87
|
+
* Being numerically larger than the in-loop watchdogs is NOT what keeps this
|
|
88
|
+
* from killing a healthy run, and treating it that way is what made it do so:
|
|
89
|
+
* this clock and the loop's `lastModelStreamProgressAt` measure DIFFERENT
|
|
90
|
+
* events. An extended-thinking phase bumps the inner clock on every engine
|
|
91
|
+
* frame while forwarding nothing, so the inner watchdog correctly stayed quiet
|
|
92
|
+
* and this one saw pure silence — runs whose worst gap crossed 150s died while
|
|
93
|
+
* still streaming, some by a single second. Ordering between two clocks only
|
|
94
|
+
* means something when they watch the same events; suspending on in-flight
|
|
95
|
+
* work is what actually makes the two agree.
|
|
96
|
+
*
|
|
86
97
|
* This is now only the CEILING, not the value: `resolveRunNoProgressTimeoutMs`
|
|
87
98
|
* clamps the foreground backstop to a fraction of the chunk's soft timeout
|
|
88
99
|
* (~30s at a 40s chunk), which is BELOW the 90s in-loop watchdogs rather than
|
|
@@ -161,6 +172,36 @@ export function resolveRunNoProgressTimeoutMs(params) {
|
|
|
161
172
|
return ceiling;
|
|
162
173
|
return override === 0 ? 0 : Math.min(override, ceiling);
|
|
163
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Work-tracking transition for one event: `1` opens a unit of in-flight work,
|
|
177
|
+
* `-1` closes one, `0` is not a work-tracking event.
|
|
178
|
+
*
|
|
179
|
+
* Every pair listed here suspends the no-progress backstop for as long as it is
|
|
180
|
+
* open, so each one MUST be bounded by a watchdog of its own — tool calls by
|
|
181
|
+
* the per-tool timeout, cross-app calls by the A2A poll timeout, the model
|
|
182
|
+
* stream by `MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS`, which the agent loop races
|
|
183
|
+
* against every wait for the next engine frame. A pair added here without such
|
|
184
|
+
* a bound turns the backstop off for the rest of the run, which is strictly
|
|
185
|
+
* worse than the stall it was meant to catch.
|
|
186
|
+
*
|
|
187
|
+
* Note what that leaves: the model-stream bound covers waiting for the engine,
|
|
188
|
+
* not a hang while the loop processes a frame it already has. Nothing here
|
|
189
|
+
* covers that segment, so a suspension is only ever as tight as the chunk's own
|
|
190
|
+
* soft timeout. Keep the pairs narrow around genuinely blocking work.
|
|
191
|
+
*/
|
|
192
|
+
function inFlightWorkDelta(event) {
|
|
193
|
+
switch (event.type) {
|
|
194
|
+
case "tool_start":
|
|
195
|
+
return 1;
|
|
196
|
+
case "tool_done":
|
|
197
|
+
return -1;
|
|
198
|
+
case "agent_call":
|
|
199
|
+
case "model_stream":
|
|
200
|
+
return event.status === "start" ? 1 : -1;
|
|
201
|
+
default:
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
164
205
|
/**
|
|
165
206
|
* Default SQL retention for completed run event logs (24 hours).
|
|
166
207
|
*
|
|
@@ -829,9 +870,10 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
829
870
|
// ── No-progress backstop (see RUN_NO_PROGRESS_HARD_TIMEOUT_MS) ──────────
|
|
830
871
|
// Timer-driven and independent of the agent loop, so it fires even when the
|
|
831
872
|
// stall is in a segment the in-loop watchdogs never see (engine-call
|
|
832
|
-
// establishment, setup, a wedged transport emitting keepalives). Tool calls
|
|
833
|
-
//
|
|
834
|
-
// emits nothing for minutes and carries its own
|
|
873
|
+
// establishment, setup, a wedged transport emitting keepalives). Tool calls,
|
|
874
|
+
// sub-agent calls, and the model stream in flight suspend it — each
|
|
875
|
+
// legitimately emits nothing for minutes and each carries a bound of its own
|
|
876
|
+
// (see `inFlightWorkDelta`).
|
|
835
877
|
let lastRealProgressAt = Date.now();
|
|
836
878
|
let inFlightWorkCount = 0;
|
|
837
879
|
let inFlightMarkerSince = null;
|
|
@@ -854,24 +896,11 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
854
896
|
});
|
|
855
897
|
};
|
|
856
898
|
const trackInFlightWork = (event) => {
|
|
899
|
+
const delta = inFlightWorkDelta(event);
|
|
900
|
+
if (delta === 0)
|
|
901
|
+
return; // Not a work-tracking event — no transition.
|
|
857
902
|
const wasIdle = inFlightWorkCount === 0;
|
|
858
|
-
|
|
859
|
-
inFlightWorkCount += 1;
|
|
860
|
-
}
|
|
861
|
-
else if (event.type === "tool_done") {
|
|
862
|
-
inFlightWorkCount = Math.max(0, inFlightWorkCount - 1);
|
|
863
|
-
}
|
|
864
|
-
else if (event.type === "agent_call") {
|
|
865
|
-
if (event.status === "start") {
|
|
866
|
-
inFlightWorkCount += 1;
|
|
867
|
-
}
|
|
868
|
-
else {
|
|
869
|
-
inFlightWorkCount = Math.max(0, inFlightWorkCount - 1);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
else {
|
|
873
|
-
return; // Not a work-tracking event — no transition possible.
|
|
874
|
-
}
|
|
903
|
+
inFlightWorkCount = Math.max(0, inFlightWorkCount + delta);
|
|
875
904
|
// Mirror the 0<->N transition into SQL so a stale reaper running in a
|
|
876
905
|
// DIFFERENT isolate (a client's SQL-subscription poll, a sibling
|
|
877
906
|
// isolate's opportunistic cleanup, a fresh boot's startup sweep) can
|
|
@@ -882,6 +911,14 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
882
911
|
// keep failures observable and preserve transition order. See
|
|
883
912
|
// `setRunInFlightMarker` / `IN_FLIGHT_RUN_STALE_GRACE_MS` in run-store.ts
|
|
884
913
|
// for the full reasoning and bounded-grace derivation.
|
|
914
|
+
//
|
|
915
|
+
// A model stream in flight mirrors the marker too, deliberately: a long
|
|
916
|
+
// thinking phase is exactly when another isolate's reaper would call this
|
|
917
|
+
// run stale, and the stream is as demonstrably alive as a tool call. It
|
|
918
|
+
// cannot latch a corpse — the grace clause additionally requires the
|
|
919
|
+
// liveness basis to be fresh within `IN_FLIGHT_GRACE_MAX_LIVENESS_GAP_MS`,
|
|
920
|
+
// so a marker left set by a dead producer buys nothing. The cost is one
|
|
921
|
+
// extra set/clear pair per model call on the way to each tool call.
|
|
885
922
|
if (wasIdle && inFlightWorkCount > 0) {
|
|
886
923
|
mirrorInFlightMarker(true);
|
|
887
924
|
}
|
|
@@ -924,7 +961,8 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
924
961
|
return;
|
|
925
962
|
if (Date.now() - lastRealProgressAt < noProgressTimeoutMs)
|
|
926
963
|
return;
|
|
927
|
-
console.error(`[run-manager] no real progress for ${noProgressTimeoutMs}ms with no tool
|
|
964
|
+
console.error(`[run-manager] no real progress for ${noProgressTimeoutMs}ms with no tool ` +
|
|
965
|
+
`or model stream in flight — ` +
|
|
928
966
|
`checkpointing run for continuation`, runId);
|
|
929
967
|
// Mirror the soft-timeout semantics exactly: the chunk completes (not
|
|
930
968
|
// aborts) at an auto_continue boundary, so the continuation machinery —
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -276,6 +276,27 @@ export type AgentChatEvent = {
|
|
|
276
276
|
text: string;
|
|
277
277
|
} | {
|
|
278
278
|
type: "stream_keepalive";
|
|
279
|
+
} | {
|
|
280
|
+
/**
|
|
281
|
+
* Lifecycle bracket around ONE engine (model) call, emitted by the agent
|
|
282
|
+
* loop when the stream is established and again — from a `finally` — when
|
|
283
|
+
* it ends, returns, or throws.
|
|
284
|
+
*
|
|
285
|
+
* Exists so the run manager's no-progress backstop can tell "the model is
|
|
286
|
+
* generating" from "nothing is happening". An extended-thinking phase
|
|
287
|
+
* emits frames that keep the loop's own 90s model-stream watchdog fresh
|
|
288
|
+
* without producing any forwarded event, so the backstop's clock saw pure
|
|
289
|
+
* silence and killed demonstrably-alive runs at 150s. `trackInFlightWork`
|
|
290
|
+
* counts this pair exactly like `tool_start`/`tool_done`: an engine call
|
|
291
|
+
* in flight suspends the backstop, bounded by the in-loop watchdog the
|
|
292
|
+
* same way a tool call is bounded by its own timeout.
|
|
293
|
+
*
|
|
294
|
+
* Deliberately NOT a keepalive: a keepalive proves the transport is up,
|
|
295
|
+
* this proves the loop is inside a model call it will be held accountable
|
|
296
|
+
* for by `MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS`.
|
|
297
|
+
*/
|
|
298
|
+
type: "model_stream";
|
|
299
|
+
status: "start" | "end";
|
|
279
300
|
} | {
|
|
280
301
|
type: "tool_start";
|
|
281
302
|
tool: string;
|
package/dist/catalog.json
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"@tiptap/extension-collaboration": "3.30.1",
|
|
15
15
|
"@tiptap/extension-collaboration-caret": "3.30.1",
|
|
16
16
|
"@tiptap/extension-color": "3.30.1",
|
|
17
|
+
"@tiptap/extension-floating-menu": "3.30.1",
|
|
17
18
|
"@tiptap/extension-horizontal-rule": "3.30.1",
|
|
18
19
|
"@tiptap/extension-image": "3.30.1",
|
|
19
20
|
"@tiptap/extension-link": "3.30.1",
|
|
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
62
62
|
error: string;
|
|
63
63
|
states?: undefined;
|
|
64
64
|
} | {
|
|
65
|
-
error?: undefined;
|
|
66
65
|
states: {
|
|
67
66
|
clientId: number;
|
|
68
67
|
state: string;
|
|
69
68
|
}[];
|
|
69
|
+
error?: undefined;
|
|
70
70
|
}>>;
|
|
71
71
|
/**
|
|
72
72
|
* GET /_agent-native/collab/:docId/users
|
|
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
|
|
|
77
77
|
error: string;
|
|
78
78
|
users?: undefined;
|
|
79
79
|
} | {
|
|
80
|
-
error?: undefined;
|
|
81
80
|
users: {
|
|
82
81
|
clientId: number;
|
|
83
82
|
lastSeen: number;
|
|
84
83
|
}[];
|
|
84
|
+
error?: undefined;
|
|
85
85
|
}>>;
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
error: string;
|
|
30
29
|
ok?: undefined;
|
|
30
|
+
error: string;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
|
@@ -14,7 +14,7 @@ import { type DatabaseToolsOption } from "./scripts/db/tool-mode.js";
|
|
|
14
14
|
* when it names them in `initialToolNames`. They stay in the searchable registry
|
|
15
15
|
* either way.
|
|
16
16
|
*/
|
|
17
|
-
export declare const FRAMEWORK_TOOL_GROUPS: readonly ["sharing", "review", "history", "featureFlags", "localization", "audit", "contextXray", "userProfile", "automation", "docs", "resources", "web", "workspaceApps", "chat", "email"];
|
|
17
|
+
export declare const FRAMEWORK_TOOL_GROUPS: readonly ["sharing", "review", "history", "featureFlags", "localization", "audit", "contextXray", "userProfile", "automation", "docs", "resources", "web", "workspaceApps", "chat", "email", "emailCatalog", "workspaceUserGroups", "orgServiceTokens"];
|
|
18
18
|
export type FrameworkToolGroup = (typeof FRAMEWORK_TOOL_GROUPS)[number];
|
|
19
19
|
/**
|
|
20
20
|
* Per-group switches for the framework's own agent tools. Every group defaults
|
|
@@ -73,6 +73,21 @@ export interface FrameworkToolsOption {
|
|
|
73
73
|
chat?: boolean;
|
|
74
74
|
/** `core-send-email`. */
|
|
75
75
|
email?: boolean;
|
|
76
|
+
/** The read-only transactional-email catalog (`list-transactional-emails`,
|
|
77
|
+
* `list-email-log`, `list-email-activity`, …). Separate from `email`, which
|
|
78
|
+
* owns the SEND capability: Dispatch asks any app what it sends without that
|
|
79
|
+
* app opting into sending from the agent, so these default to on. */
|
|
80
|
+
emailCatalog?: boolean;
|
|
81
|
+
/** Reusable workspace member lists (`list-workspace-user-groups`,
|
|
82
|
+
* `upsert-workspace-user-group`, `bulk-update-workspace-user-groups`,
|
|
83
|
+
* `delete-workspace-user-group`). Their UI is the Team page and the share
|
|
84
|
+
* dialog; an app with neither has no use for them. */
|
|
85
|
+
workspaceUserGroups?: boolean;
|
|
86
|
+
/** `create-org-service-token`, `list-org-service-tokens`,
|
|
87
|
+
* `revoke-org-service-token`. Minting a credential is a real capability:
|
|
88
|
+
* `mcp.enabled` decides whether the ROUTES exist, this decides whether the
|
|
89
|
+
* model can call them. */
|
|
90
|
+
orgServiceTokens?: boolean;
|
|
76
91
|
/** `"minimal"` turns every group above off, for voice-first and
|
|
77
92
|
* single-purpose apps that want the template's own actions and nothing else.
|
|
78
93
|
* Any explicit group key wins over the preset, so
|
package/dist/framework-tools.js
CHANGED
|
@@ -30,6 +30,9 @@ export const FRAMEWORK_TOOL_GROUPS = [
|
|
|
30
30
|
"workspaceApps",
|
|
31
31
|
"chat",
|
|
32
32
|
"email",
|
|
33
|
+
"emailCatalog",
|
|
34
|
+
"workspaceUserGroups",
|
|
35
|
+
"orgServiceTokens",
|
|
33
36
|
];
|
|
34
37
|
function normalizeFrameworkToolsConfig(config) {
|
|
35
38
|
if (config === "minimal")
|
|
@@ -152,6 +155,18 @@ export const CORE_ACTION_GROUPS = {
|
|
|
152
155
|
"get-resource-version": "history",
|
|
153
156
|
"restore-resource-version": "history",
|
|
154
157
|
"list-resource-history": "history",
|
|
158
|
+
"list-transactional-emails": "emailCatalog",
|
|
159
|
+
"render-transactional-email-preview": "emailCatalog",
|
|
160
|
+
"list-email-log": "emailCatalog",
|
|
161
|
+
"list-email-activity": "emailCatalog",
|
|
162
|
+
"list-email-engagement": "emailCatalog",
|
|
163
|
+
"list-workspace-user-groups": "workspaceUserGroups",
|
|
164
|
+
"upsert-workspace-user-group": "workspaceUserGroups",
|
|
165
|
+
"bulk-update-workspace-user-groups": "workspaceUserGroups",
|
|
166
|
+
"delete-workspace-user-group": "workspaceUserGroups",
|
|
167
|
+
"create-org-service-token": "orgServiceTokens",
|
|
168
|
+
"list-org-service-tokens": "orgServiceTokens",
|
|
169
|
+
"revoke-org-service-token": "orgServiceTokens",
|
|
155
170
|
"list-review-comments": "review",
|
|
156
171
|
"create-review-comment": "review",
|
|
157
172
|
"reply-review-comment": "review",
|
|
@@ -10,6 +10,7 @@ import { resolveAutomationExecutionIdentity, } from "../automations/service.js";
|
|
|
10
10
|
import { createThread, getThread, updateThreadData, withThreadDataLock, } from "../chat-threads/store.js";
|
|
11
11
|
import { queryOrgMembers } from "../org/context.js";
|
|
12
12
|
import { organizationIdFromResourceOwner, organizationResourceOwner, } from "../resources/store.js";
|
|
13
|
+
import { captureError } from "../server/capture-error.js";
|
|
13
14
|
import { runWithRequestContext, } from "../server/request-context.js";
|
|
14
15
|
import { attachAutomationRunThread, finishAutomationRun, startAutomationRun, } from "./run-history.js";
|
|
15
16
|
const BACKGROUND_RUN_STUCK_MS = 10 * 60_000;
|
|
@@ -177,11 +178,32 @@ export async function runBackgroundAutomation(options, deps) {
|
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
let result;
|
|
181
|
+
// Populated as soon as the run id exists, so a failure that never returns a
|
|
182
|
+
// result can still be joined to its LLM trace (`aiTraceId` -> $ai_trace_id).
|
|
183
|
+
const runIdRef = { current: null };
|
|
180
184
|
try {
|
|
181
|
-
result = await executeBackgroundAutomation(options, deps, historyId);
|
|
185
|
+
result = await executeBackgroundAutomation(options, deps, historyId, runIdRef);
|
|
182
186
|
}
|
|
183
187
|
catch (err) {
|
|
184
188
|
const message = err instanceof Error ? err.message : String(err);
|
|
189
|
+
// Both callers (recurring-jobs scheduler, trigger dispatcher) record this
|
|
190
|
+
// onto the automation's own metadata and console.error it, and neither
|
|
191
|
+
// reports it. A failure visible only in a resource field and stdout is not
|
|
192
|
+
// a failure anyone sees: the run-level no-progress cutoff killed
|
|
193
|
+
// automations across two releases without ever raising an issue.
|
|
194
|
+
captureError(err, {
|
|
195
|
+
tags: {
|
|
196
|
+
area: "background-automation",
|
|
197
|
+
automation: automation.name,
|
|
198
|
+
scope: options.orgId ? "organization" : "personal",
|
|
199
|
+
},
|
|
200
|
+
extra: {
|
|
201
|
+
automationPath: automation.resource.path,
|
|
202
|
+
appId: deps.appId,
|
|
203
|
+
historyId,
|
|
204
|
+
},
|
|
205
|
+
...(runIdRef.current ? { aiTraceId: runIdRef.current } : {}),
|
|
206
|
+
});
|
|
185
207
|
await recordRunOutcome(historyId, "error", `${message}. No delivery was confirmed.`);
|
|
186
208
|
throw err;
|
|
187
209
|
}
|
|
@@ -289,7 +311,7 @@ async function recordRunOutcome(historyId, status, error) {
|
|
|
289
311
|
console.error(`[automations] Could not record run ${historyId} as ${status}:`, err);
|
|
290
312
|
}
|
|
291
313
|
}
|
|
292
|
-
async function executeBackgroundAutomation(options, deps, historyId) {
|
|
314
|
+
async function executeBackgroundAutomation(options, deps, historyId, runIdRef) {
|
|
293
315
|
const { automation, ownerEmail, orgId, prompt, threadTitle, usageLabel } = options;
|
|
294
316
|
return runWithRequestContext({
|
|
295
317
|
...options.requestContext,
|
|
@@ -335,6 +357,8 @@ async function executeBackgroundAutomation(options, deps, historyId) {
|
|
|
335
357
|
orgId: orgId ?? null,
|
|
336
358
|
});
|
|
337
359
|
const runId = createRunId(options.runIdPrefix);
|
|
360
|
+
if (runIdRef)
|
|
361
|
+
runIdRef.current = runId;
|
|
338
362
|
await recordRunThread(historyId, thread.id, runId);
|
|
339
363
|
// Scheduled work is background work: it has no synchronous serverless
|
|
340
364
|
// caller waiting on it, so it must not inherit the interactive clamp
|
|
@@ -137,13 +137,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
137
137
|
inputSchema: {
|
|
138
138
|
type: string;
|
|
139
139
|
properties: {
|
|
140
|
+
count?: undefined;
|
|
140
141
|
query?: undefined;
|
|
141
142
|
minutes?: undefined;
|
|
142
143
|
limit?: undefined;
|
|
143
144
|
clientHint?: undefined;
|
|
144
145
|
timestamp?: undefined;
|
|
145
146
|
chapterId?: undefined;
|
|
146
|
-
count?: undefined;
|
|
147
147
|
startAt?: undefined;
|
|
148
148
|
endAt?: undefined;
|
|
149
149
|
reason?: undefined;
|
|
@@ -159,6 +159,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
159
159
|
inputSchema: {
|
|
160
160
|
type: string;
|
|
161
161
|
properties: {
|
|
162
|
+
count?: undefined;
|
|
162
163
|
query: {
|
|
163
164
|
type: string;
|
|
164
165
|
description: string;
|
|
@@ -174,7 +175,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
174
175
|
clientHint?: undefined;
|
|
175
176
|
timestamp?: undefined;
|
|
176
177
|
chapterId?: undefined;
|
|
177
|
-
count?: undefined;
|
|
178
178
|
startAt?: undefined;
|
|
179
179
|
endAt?: undefined;
|
|
180
180
|
reason?: undefined;
|
|
@@ -190,6 +190,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
190
190
|
inputSchema: {
|
|
191
191
|
type: string;
|
|
192
192
|
properties: {
|
|
193
|
+
count?: undefined;
|
|
193
194
|
minutes: {
|
|
194
195
|
type: string;
|
|
195
196
|
description: string;
|
|
@@ -199,7 +200,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
199
200
|
clientHint?: undefined;
|
|
200
201
|
timestamp?: undefined;
|
|
201
202
|
chapterId?: undefined;
|
|
202
|
-
count?: undefined;
|
|
203
203
|
startAt?: undefined;
|
|
204
204
|
endAt?: undefined;
|
|
205
205
|
reason?: undefined;
|
|
@@ -216,6 +216,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
216
216
|
type: string;
|
|
217
217
|
required: string[];
|
|
218
218
|
properties: {
|
|
219
|
+
count?: undefined;
|
|
219
220
|
query: {
|
|
220
221
|
type: string;
|
|
221
222
|
description: string;
|
|
@@ -234,7 +235,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
234
235
|
};
|
|
235
236
|
timestamp?: undefined;
|
|
236
237
|
chapterId?: undefined;
|
|
237
|
-
count?: undefined;
|
|
238
238
|
startAt?: undefined;
|
|
239
239
|
endAt?: undefined;
|
|
240
240
|
reason?: undefined;
|
|
@@ -250,6 +250,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
250
250
|
type: string;
|
|
251
251
|
required: string[];
|
|
252
252
|
properties: {
|
|
253
|
+
count?: undefined;
|
|
253
254
|
query?: undefined;
|
|
254
255
|
minutes?: undefined;
|
|
255
256
|
limit?: undefined;
|
|
@@ -263,7 +264,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
263
264
|
description: string;
|
|
264
265
|
};
|
|
265
266
|
chapterId?: undefined;
|
|
266
|
-
count?: undefined;
|
|
267
267
|
startAt?: undefined;
|
|
268
268
|
endAt?: undefined;
|
|
269
269
|
includeMicrophone?: undefined;
|
|
@@ -314,13 +314,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
314
314
|
type: string;
|
|
315
315
|
required: string[];
|
|
316
316
|
properties: {
|
|
317
|
+
count?: undefined;
|
|
317
318
|
query?: undefined;
|
|
318
319
|
minutes?: undefined;
|
|
319
320
|
limit?: undefined;
|
|
320
321
|
clientHint?: undefined;
|
|
321
322
|
timestamp?: undefined;
|
|
322
323
|
chapterId?: undefined;
|
|
323
|
-
count?: undefined;
|
|
324
324
|
startAt: {
|
|
325
325
|
type: string;
|
|
326
326
|
description: string;
|
|
@@ -349,13 +349,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
349
349
|
type: string;
|
|
350
350
|
required: string[];
|
|
351
351
|
properties: {
|
|
352
|
+
count?: undefined;
|
|
352
353
|
query?: undefined;
|
|
353
354
|
minutes?: undefined;
|
|
354
355
|
limit?: undefined;
|
|
355
356
|
clientHint?: undefined;
|
|
356
357
|
timestamp?: undefined;
|
|
357
358
|
chapterId?: undefined;
|
|
358
|
-
count?: undefined;
|
|
359
359
|
startAt?: undefined;
|
|
360
360
|
endAt?: undefined;
|
|
361
361
|
reason?: undefined;
|
|
@@ -11,23 +11,23 @@
|
|
|
11
11
|
* DELETE /_agent-native/notifications/:id — delete
|
|
12
12
|
*/
|
|
13
13
|
export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
|
|
14
|
-
error?: undefined;
|
|
15
14
|
count: number;
|
|
16
15
|
updated?: undefined;
|
|
17
16
|
ok?: undefined;
|
|
18
|
-
} | {
|
|
19
17
|
error?: undefined;
|
|
18
|
+
} | {
|
|
19
|
+
count?: undefined;
|
|
20
20
|
updated: number;
|
|
21
21
|
ok?: undefined;
|
|
22
|
-
|
|
22
|
+
error?: undefined;
|
|
23
23
|
} | {
|
|
24
|
+
count?: undefined;
|
|
24
25
|
updated?: undefined;
|
|
25
26
|
error: string;
|
|
26
27
|
ok?: undefined;
|
|
27
|
-
count?: undefined;
|
|
28
28
|
} | {
|
|
29
|
-
|
|
29
|
+
count?: undefined;
|
|
30
30
|
updated?: undefined;
|
|
31
31
|
ok: boolean;
|
|
32
|
-
|
|
32
|
+
error?: undefined;
|
|
33
33
|
}>>;
|
|
@@ -24,6 +24,17 @@ function costUsdFromCenticents(value) {
|
|
|
24
24
|
return Math.round((value / 10_000) * 1_000_000) / 1_000_000;
|
|
25
25
|
}
|
|
26
26
|
const MAX_TRACKED_GENERATION_TOOL_CALLS = 50;
|
|
27
|
+
/**
|
|
28
|
+
* `auto_continue` reasons the server PLANNED, which must not read as failures.
|
|
29
|
+
*
|
|
30
|
+
* A hosted foreground chunk ends at `run_timeout` roughly every 40s by design —
|
|
31
|
+
* counting those as errors would bury the boundaries that mean something under
|
|
32
|
+
* the ones that mean "working as intended". Every other reason is a boundary
|
|
33
|
+
* something forced on the run: recoverable, but not normal, and it stays
|
|
34
|
+
* visible as an error carrying its reason as the terminal code. Moving a reason
|
|
35
|
+
* across this line changes what the error rate means, so move it deliberately.
|
|
36
|
+
*/
|
|
37
|
+
const EXPECTED_CONTINUATION_REASONS = new Set(["run_timeout", "auto_continue"]);
|
|
27
38
|
const MAX_TOOL_ERROR_MESSAGE_LENGTH = 500;
|
|
28
39
|
const STANDALONE_API_KEY_PATTERN = /\b(?:sk-(?:proj-|ant-)?[A-Za-z0-9_-]{8,}|(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{8,}|AIza[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{16,})\b/g;
|
|
29
40
|
function truncateToolErrorMessage(value) {
|
|
@@ -333,6 +344,11 @@ export async function instrumentAgentLoop(opts) {
|
|
|
333
344
|
let errorMessage = null;
|
|
334
345
|
let runMetadata = opts.metadata ?? null;
|
|
335
346
|
let terminalOutcome;
|
|
347
|
+
// The `auto_continue` boundary this run ended at, if any. A cut-off never
|
|
348
|
+
// reaches the loop's outcome classification (`runAgentLoop` returns early at
|
|
349
|
+
// the checkpoint), so without this the run reports no terminal state at all
|
|
350
|
+
// and the reason is recoverable only from `agent_run_events` in Postgres.
|
|
351
|
+
let cutOffReason = null;
|
|
336
352
|
const instrumentedOutcome = (outcome) => {
|
|
337
353
|
terminalOutcome = outcome;
|
|
338
354
|
if (outcome.state === "completed") {
|
|
@@ -377,6 +393,15 @@ export async function instrumentAgentLoop(opts) {
|
|
|
377
393
|
if (event.type === "clear" || event.type === "done") {
|
|
378
394
|
runStatus = "success";
|
|
379
395
|
errorMessage = null;
|
|
396
|
+
cutOffReason = null;
|
|
397
|
+
}
|
|
398
|
+
else if (event.type === "auto_continue") {
|
|
399
|
+
const reason = event.reason || "auto_continue";
|
|
400
|
+
cutOffReason = reason;
|
|
401
|
+
if (!EXPECTED_CONTINUATION_REASONS.has(reason)) {
|
|
402
|
+
runStatus = "error";
|
|
403
|
+
errorMessage = `Agent run was cut off before finishing (${reason}).`;
|
|
404
|
+
}
|
|
380
405
|
}
|
|
381
406
|
else if (event.type === "error") {
|
|
382
407
|
runStatus = "error";
|
|
@@ -635,6 +660,20 @@ export async function instrumentAgentLoop(opts) {
|
|
|
635
660
|
}
|
|
636
661
|
}
|
|
637
662
|
catch { }
|
|
663
|
+
// A cut-off run never reaches the loop's outcome classification, so stand in
|
|
664
|
+
// for it here rather than reporting no terminal state at all. `failed` +
|
|
665
|
+
// `retryable` is the honest encoding available in `AgentLoopOutcome`: the
|
|
666
|
+
// turn did not finish, and the continuation machinery is expected to
|
|
667
|
+
// recover it. A real reported outcome always wins.
|
|
668
|
+
const effectiveTerminalOutcome = terminalOutcome ??
|
|
669
|
+
(cutOffReason && !EXPECTED_CONTINUATION_REASONS.has(cutOffReason)
|
|
670
|
+
? {
|
|
671
|
+
state: "failed",
|
|
672
|
+
code: cutOffReason,
|
|
673
|
+
retryable: true,
|
|
674
|
+
message: `Agent run was cut off before finishing (${cutOffReason}).`,
|
|
675
|
+
}
|
|
676
|
+
: undefined);
|
|
638
677
|
let llmCallCount = 0;
|
|
639
678
|
if (usage || runStatus === "error") {
|
|
640
679
|
llmCallCount =
|
|
@@ -716,7 +755,7 @@ export async function instrumentAgentLoop(opts) {
|
|
|
716
755
|
.sort(([a], [b]) => a - b)
|
|
717
756
|
.map(([, detail]) => detail),
|
|
718
757
|
toolsTruncated: toolInvocationCounter > MAX_TRACKED_GENERATION_TOOL_CALLS,
|
|
719
|
-
terminalOutcome,
|
|
758
|
+
terminalOutcome: effectiveTerminalOutcome,
|
|
720
759
|
delegation: opts.delegation,
|
|
721
760
|
createdAt: runStart,
|
|
722
761
|
experimentAssignments: opts.experimentAssignments,
|
|
@@ -752,13 +791,13 @@ export async function instrumentAgentLoop(opts) {
|
|
|
752
791
|
try {
|
|
753
792
|
const aiError = runStatus === "error"
|
|
754
793
|
? toAiErrorDetail(errorMessage, {
|
|
755
|
-
state:
|
|
756
|
-
code:
|
|
757
|
-
|
|
758
|
-
?
|
|
794
|
+
state: effectiveTerminalOutcome?.state,
|
|
795
|
+
code: effectiveTerminalOutcome?.state === "failed" ||
|
|
796
|
+
effectiveTerminalOutcome?.state === "input_required"
|
|
797
|
+
? effectiveTerminalOutcome.code
|
|
759
798
|
: undefined,
|
|
760
|
-
retryable:
|
|
761
|
-
?
|
|
799
|
+
retryable: effectiveTerminalOutcome?.state === "failed"
|
|
800
|
+
? effectiveTerminalOutcome.retryable
|
|
762
801
|
: undefined,
|
|
763
802
|
})
|
|
764
803
|
: undefined;
|
|
@@ -792,6 +831,10 @@ export async function instrumentAgentLoop(opts) {
|
|
|
792
831
|
source: "agent_observability",
|
|
793
832
|
run_id: runId,
|
|
794
833
|
thread_id: threadId,
|
|
834
|
+
// Present for planned boundaries too, which are not errors: the ratio
|
|
835
|
+
// of run_timeout to no_progress is the signal, and it is unreadable
|
|
836
|
+
// if only one side of it is recorded.
|
|
837
|
+
...(cutOffReason ? { terminal_reason: cutOffReason } : {}),
|
|
795
838
|
// A truncated run must not read as a complete one.
|
|
796
839
|
...(droppedToolSpans > 0
|
|
797
840
|
? {
|
|
@@ -897,10 +940,10 @@ export async function instrumentAgentLoop(opts) {
|
|
|
897
940
|
"agent.input_tokens": usage?.inputTokens ?? 0,
|
|
898
941
|
"agent.output_tokens": usage?.outputTokens ?? 0,
|
|
899
942
|
"agent.cost_cents_x100": costCentsX100,
|
|
900
|
-
"agent.terminal_state":
|
|
901
|
-
"agent.terminal_code":
|
|
902
|
-
|
|
903
|
-
?
|
|
943
|
+
"agent.terminal_state": effectiveTerminalOutcome?.state,
|
|
944
|
+
"agent.terminal_code": effectiveTerminalOutcome?.state === "failed" ||
|
|
945
|
+
effectiveTerminalOutcome?.state === "input_required"
|
|
946
|
+
? effectiveTerminalOutcome.code
|
|
904
947
|
: undefined,
|
|
905
948
|
},
|
|
906
949
|
});
|
|
@@ -51,8 +51,8 @@ export declare function handleDeleteResource(event: any): Promise<{
|
|
|
51
51
|
error: string;
|
|
52
52
|
ok?: undefined;
|
|
53
53
|
} | {
|
|
54
|
-
ok: boolean;
|
|
55
54
|
error?: undefined;
|
|
55
|
+
ok: boolean;
|
|
56
56
|
}>;
|
|
57
57
|
/** POST /_agent-native/resources/upload — upload a file as a resource */
|
|
58
58
|
export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
|
|
@@ -73,10 +73,10 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
|
|
|
73
73
|
runId: string | null;
|
|
74
74
|
expiresAt: number | null;
|
|
75
75
|
metadata: string | null;
|
|
76
|
+
error?: undefined;
|
|
76
77
|
url: string;
|
|
77
78
|
provider: string;
|
|
78
79
|
storageSetupRequired?: undefined;
|
|
79
|
-
error?: undefined;
|
|
80
80
|
} | {
|
|
81
81
|
error: string;
|
|
82
82
|
storageSetupRequired: boolean;
|
|
@@ -86,10 +86,17 @@ export { CORE_ACTION_GROUPS };
|
|
|
86
86
|
* Core actions with no `frameworkTools` switch, and why:
|
|
87
87
|
*
|
|
88
88
|
* - `upload-image` is load-bearing for ordinary work.
|
|
89
|
-
* -
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
89
|
+
* - MCP tools and the hosted-harness routes never enter the model's action
|
|
90
|
+
* surface at all (`agentTool: false`), so a switch would gate nothing.
|
|
91
|
+
*
|
|
92
|
+
* Membership is expensive in a way that is easy to miss: an untagged action is
|
|
93
|
+
* also in every app's DEFAULT first-request tool list (see
|
|
94
|
+
* `resolveInitialToolNames`), so each entry here is a schema every app pays for
|
|
95
|
+
* on turn one whether or not it has the surface. The email catalog, the
|
|
96
|
+
* workspace user groups, and the org service tokens each sat here for that
|
|
97
|
+
* reason alone and now answer to `emailCatalog`, `workspaceUserGroups`, and
|
|
98
|
+
* `orgServiceTokens` — all defaulting to on, so availability is unchanged, but
|
|
99
|
+
* reachable through `tool-search` instead of riding along in every request.
|
|
93
100
|
*/
|
|
94
101
|
export declare const ALWAYS_ON_CORE_ACTIONS: ReadonlySet<string>;
|
|
95
102
|
export declare function mergeCoreSharingActions(registry: Record<string, ActionEntry>): Promise<void>;
|
|
@@ -508,25 +508,20 @@ export { CORE_ACTION_GROUPS };
|
|
|
508
508
|
* Core actions with no `frameworkTools` switch, and why:
|
|
509
509
|
*
|
|
510
510
|
* - `upload-image` is load-bearing for ordinary work.
|
|
511
|
-
* -
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
511
|
+
* - MCP tools and the hosted-harness routes never enter the model's action
|
|
512
|
+
* surface at all (`agentTool: false`), so a switch would gate nothing.
|
|
513
|
+
*
|
|
514
|
+
* Membership is expensive in a way that is easy to miss: an untagged action is
|
|
515
|
+
* also in every app's DEFAULT first-request tool list (see
|
|
516
|
+
* `resolveInitialToolNames`), so each entry here is a schema every app pays for
|
|
517
|
+
* on turn one whether or not it has the surface. The email catalog, the
|
|
518
|
+
* workspace user groups, and the org service tokens each sat here for that
|
|
519
|
+
* reason alone and now answer to `emailCatalog`, `workspaceUserGroups`, and
|
|
520
|
+
* `orgServiceTokens` — all defaulting to on, so availability is unchanged, but
|
|
521
|
+
* reachable through `tool-search` instead of riding along in every request.
|
|
515
522
|
*/
|
|
516
523
|
export const ALWAYS_ON_CORE_ACTIONS = new Set([
|
|
517
524
|
"upload-image",
|
|
518
|
-
"list-workspace-user-groups",
|
|
519
|
-
"upsert-workspace-user-group",
|
|
520
|
-
"bulk-update-workspace-user-groups",
|
|
521
|
-
"delete-workspace-user-group",
|
|
522
|
-
"list-transactional-emails",
|
|
523
|
-
"render-transactional-email-preview",
|
|
524
|
-
"list-email-log",
|
|
525
|
-
"list-email-activity",
|
|
526
|
-
"list-email-engagement",
|
|
527
|
-
"create-org-service-token",
|
|
528
|
-
"list-org-service-tokens",
|
|
529
|
-
"revoke-org-service-token",
|
|
530
525
|
"list-mcp-tools",
|
|
531
526
|
"call-mcp-tool",
|
|
532
527
|
// Hosted harness capability/policy routes are UI-facing and deliberately
|
|
@@ -36,10 +36,25 @@ export function resolveAgentChatMcpOptions(input) {
|
|
|
36
36
|
// otherwise `disableMcp: true` + `enabled: false` would read as a conflict.
|
|
37
37
|
const legacyEnabled = input?.disableMcp === undefined ? undefined : !input.disableMcp;
|
|
38
38
|
const enabled = pick("enabled", "disableMcp", legacyEnabled, mcp.enabled);
|
|
39
|
+
const connectorCatalog = pick("connectorCatalog", "connectorCatalog", input?.connectorCatalog, mcp.connectorCatalog);
|
|
40
|
+
// `catalogMode: "app"` short-circuits `connectorCatalogActive` in
|
|
41
|
+
// `build-server.ts`, so declaring both served the app's FULL registry while
|
|
42
|
+
// the allow-list sat in the config looking authoritative. That is the same
|
|
43
|
+
// unexplainable-report shape `pick` throws on, minus the legacy/nested pair:
|
|
44
|
+
// an allow-list is written to keep something off the external surface, and
|
|
45
|
+
// silently ignoring it hands every action schema to every connector client.
|
|
46
|
+
if (mcp.catalog === "app" &&
|
|
47
|
+
connectorCatalog &&
|
|
48
|
+
connectorCatalog.length > 0) {
|
|
49
|
+
throw new Error(`[agent-native] Conflicting agent-chat options: \`mcp.catalog: "app"\` serves this ` +
|
|
50
|
+
`app's entire action registry flat, which makes \`mcp.connectorCatalog\` ` +
|
|
51
|
+
`(${connectorCatalog.length} name(s)) inert. Keep \`connectorCatalog\` for a curated ` +
|
|
52
|
+
`external surface, or drop it and keep \`catalog: "app"\` — not both.`);
|
|
53
|
+
}
|
|
39
54
|
return {
|
|
40
55
|
enabled: enabled ?? true,
|
|
41
56
|
catalog: mcp.catalog,
|
|
42
|
-
connectorCatalog
|
|
57
|
+
connectorCatalog,
|
|
43
58
|
externalAgents: pick("externalAgents", "externalAgents", input?.externalAgents, mcp.externalAgents),
|
|
44
59
|
builtinCrossAppTools: mcp.builtinCrossAppTools,
|
|
45
60
|
title: pick("title", "mcpServerInfo.title", legacyInfo?.title, mcp.title),
|
|
@@ -201,6 +201,8 @@ export default createAgentChatPlugin({
|
|
|
201
201
|
|
|
202
202
|
The builtin cross-app tools (`list_apps`, `open_app`, `ask_app`, `create_embed_session`, `create_workspace_app`, `list_templates`) are always included regardless of the declared list.
|
|
203
203
|
|
|
204
|
+
`mcp.catalog: "app"` is the opposite choice: it serves this app's entire action registry flat, with no compact or connector trimming. The two are mutually exclusive — declaring both throws at plugin init rather than silently ignoring the allow-list, since an allow-list exists to keep something off the external surface.
|
|
205
|
+
|
|
204
206
|
## Tool exposure once connected {#tool-exposure}
|
|
205
207
|
|
|
206
208
|
Once an agent is connected, it sees the [compact catalog](#catalog-tiers) by default: template-declared app actions plus the builtin cross-app verbs (`list_apps`, `open_app`, `ask_app`, and the app-only embed helper). Use `ask_app` to route a natural-language task through an app agent (the same cross-app entry point [A2A](/docs/a2a-protocol) uses).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.0.0-beta-
|
|
3
|
+
"version": "0.0.0-beta-20260820150521",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -428,7 +428,7 @@
|
|
|
428
428
|
"yjs": "^13.6.32",
|
|
429
429
|
"zod": "^4.3.6",
|
|
430
430
|
"@agent-native/recap-cli": "0.5.5",
|
|
431
|
-
"@agent-native/toolkit": "^0.
|
|
431
|
+
"@agent-native/toolkit": "^0.0.0-beta-20260820150521"
|
|
432
432
|
},
|
|
433
433
|
"devDependencies": {
|
|
434
434
|
"@ai-sdk/anthropic": "^3.0.71",
|