@agent-native/core 0.227.0 → 0.228.0-nightly-20261111004142
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.d.ts +6 -2
- package/dist/agent/engine/builder-engine.js +6 -1
- package/dist/agent/engine/tool-call-journal-seed.d.ts +2 -0
- package/dist/agent/engine/tool-call-journal-seed.js +14 -0
- package/dist/agent/harness/store.js +14 -0
- package/dist/agent/production-agent.js +257 -17
- package/dist/agent/reaped-run-thread.js +6 -0
- package/dist/agent/run-manager.js +6 -0
- package/dist/agent/tool-call-journal.d.ts +4 -0
- package/dist/agent/tool-call-journal.js +12 -0
- package/dist/agent/types.d.ts +7 -1
- package/dist/app-config/agent.d.ts +3 -0
- package/dist/app-config/agent.js +7 -0
- package/dist/app-config/schema.d.ts +3 -0
- package/dist/application-state/handlers.d.ts +3 -3
- package/dist/application-state/handlers.js +6 -1
- package/dist/application-state/index.d.ts +3 -2
- package/dist/application-state/index.js +3 -2
- package/dist/application-state/last-used-tab.d.ts +11 -0
- package/dist/application-state/last-used-tab.js +43 -0
- package/dist/application-state/script-helpers.d.ts +26 -0
- package/dist/application-state/script-helpers.js +83 -15
- package/dist/application-state/store.d.ts +8 -0
- package/dist/application-state/store.js +16 -0
- package/dist/cli/agent.js +7 -3
- package/dist/cli/code-agent-executor.d.ts +1 -0
- package/dist/cli/code-agent-executor.js +37 -2
- package/dist/cli/code.js +3 -1
- package/dist/client/agent-sidebar-state.d.ts +2 -0
- package/dist/client/browser-tab-use.d.ts +9 -0
- package/dist/client/browser-tab-use.js +112 -0
- package/dist/client/chat/runtime.js +3 -1
- package/dist/client/error-capture.js +33 -1
- package/dist/client/navigation/index.d.ts +1 -0
- package/dist/client/navigation/index.js +1 -0
- package/dist/client/resources/mcp-integration-logos.js +16 -0
- package/dist/client/review/use-review.d.ts +28 -0
- package/dist/client/review/use-review.js +41 -12
- package/dist/client/route-state.js +2 -0
- package/dist/client/sse-event-processor.d.ts +1 -0
- package/dist/client/sse-event-processor.js +7 -1
- package/dist/client/use-db-sync.d.ts +8 -1
- package/dist/client/use-db-sync.js +6 -5
- package/dist/coding-tools/run-code.js +12 -2
- package/dist/coding-tools/sandbox/executions-store.d.ts +1 -1
- package/dist/coding-tools/sandbox/executions-store.js +9 -3
- package/dist/collab/routes.d.ts +1 -1
- package/dist/data-programs/execute.js +1 -1
- package/dist/db/schema-snapshot.generated.js +17 -3
- package/dist/extensions/url-safety.d.ts +1 -3
- package/dist/extensions/url-safety.js +23 -23
- package/dist/file-upload/actions/upload-image.d.ts +5 -5
- package/dist/file-upload/index.d.ts +1 -1
- package/dist/file-upload/index.js +1 -1
- package/dist/file-upload/s3-client.d.ts +1 -0
- package/dist/file-upload/s3-client.js +70 -13
- package/dist/file-upload/s3.d.ts +9 -0
- package/dist/file-upload/s3.js +14 -1
- package/dist/framework-package-versions.json +8 -8
- package/dist/jobs/run-history.js +10 -3
- package/dist/jobs/scheduler.js +5 -36
- package/dist/localization/core-messages/supplemental/ar-SA.js +35 -0
- package/dist/localization/core-messages/supplemental/de-DE.js +35 -0
- package/dist/localization/core-messages/supplemental/en-US.js +35 -0
- package/dist/localization/core-messages/supplemental/es-ES.js +35 -0
- package/dist/localization/core-messages/supplemental/fr-FR.js +35 -0
- package/dist/localization/core-messages/supplemental/hi-IN.js +35 -0
- package/dist/localization/core-messages/supplemental/ja-JP.js +35 -0
- package/dist/localization/core-messages/supplemental/ko-KR.js +35 -0
- package/dist/localization/core-messages/supplemental/pt-BR.js +35 -0
- package/dist/localization/core-messages/supplemental/zh-CN.js +35 -0
- package/dist/localization/core-messages/supplemental/zh-TW.js +35 -0
- package/dist/localization/default-messages.d.ts +35 -0
- package/dist/localization/default-messages.js +35 -0
- package/dist/localization/mcp-settings-messages.d.ts +35 -0
- package/dist/mcp/actions/list-mcp-client-connections.d.ts +8 -0
- package/dist/mcp/actions/list-mcp-client-connections.js +39 -0
- package/dist/mcp/browser-tab-target.d.ts +16 -0
- package/dist/mcp/browser-tab-target.js +58 -0
- package/dist/mcp/build-server.d.ts +1 -0
- package/dist/mcp/build-server.js +38 -7
- package/dist/mcp/call-attribution.d.ts +4 -1
- package/dist/mcp/call-attribution.js +3 -1
- package/dist/mcp/client-connections.d.ts +6 -0
- package/dist/mcp/client-connections.js +91 -0
- package/dist/mcp/connect-route.js +12 -2
- package/dist/mcp/connect-store.d.ts +3 -0
- package/dist/mcp/connect-store.js +10 -1
- package/dist/mcp/direct-action-call.js +1 -0
- package/dist/mcp/oauth-store.d.ts +6 -0
- package/dist/mcp/oauth-store.js +23 -0
- package/dist/mcp/oauth-token.d.ts +1 -0
- package/dist/mcp/oauth-token.js +1 -0
- package/dist/mcp/server.js +9 -20
- package/dist/mcp-client/config.d.ts +2 -0
- package/dist/mcp-client/manager.js +23 -0
- package/dist/mcp-client/workspace-servers.js +42 -10
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/handlers.js +21 -17
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/index.js +17 -36
- package/dist/review/actions/create-review-comment.d.ts +7 -0
- package/dist/review/actions/create-review-comment.js +29 -3
- package/dist/review/actions/delete-review-comment.js +2 -2
- package/dist/review/actions/list-review-comments.d.ts +3 -0
- package/dist/review/actions/list-review-comments.js +41 -17
- package/dist/review/actions/react-to-review-comment.js +2 -2
- package/dist/review/actions/reply-review-comment.d.ts +4 -0
- package/dist/review/actions/reply-review-comment.js +15 -6
- package/dist/review/actions/update-review-comment-anchor.d.ts +6 -0
- package/dist/review/actions/update-review-comment-anchor.js +39 -10
- package/dist/review/actions/update-review-comment.d.ts +3 -0
- package/dist/review/actions/update-review-comment.js +28 -3
- package/dist/review/errors.d.ts +33 -0
- package/dist/review/errors.js +51 -0
- package/dist/review/index.d.ts +5 -4
- package/dist/review/index.js +3 -2
- package/dist/review/registry.d.ts +51 -1
- package/dist/review/registry.js +117 -0
- package/dist/review/store.d.ts +59 -14
- package/dist/review/store.js +622 -101
- package/dist/review/suggestions/actions.js +65 -10
- package/dist/review/suggestions/actor-kind.d.ts +5 -0
- package/dist/review/suggestions/actor-kind.js +20 -0
- package/dist/review/suggestions/store.js +16 -2
- package/dist/review/suggestions/types.d.ts +13 -0
- package/dist/review/types.d.ts +89 -0
- package/dist/scripts/db/dev-query-proxy.d.ts +2 -0
- package/dist/scripts/db/dev-query-proxy.js +4 -1
- package/dist/scripts/db/query.js +3 -0
- package/dist/scripts/dev/shell.js +4 -0
- package/dist/scripts/dev-session.d.ts +31 -0
- package/dist/scripts/dev-session.js +58 -0
- package/dist/scripts/resources/list.js +53 -4
- package/dist/scripts/runner.js +22 -4
- package/dist/search/query.d.ts +11 -5
- package/dist/search/query.js +52 -15
- package/dist/search/tokenize.d.ts +1 -0
- package/dist/search/tokenize.js +3 -0
- package/dist/secrets/api-keys.d.ts +4 -0
- package/dist/secrets/api-keys.js +5 -0
- package/dist/secrets/register.d.ts +2 -0
- package/dist/secrets/routes.d.ts +2 -2
- package/dist/secrets/routes.js +85 -6
- package/dist/secrets/storage.d.ts +2 -0
- package/dist/secrets/storage.js +34 -9
- package/dist/secrets/substitution.d.ts +3 -2
- package/dist/secrets/substitution.js +56 -7
- package/dist/server/action-discovery.js +5 -0
- package/dist/server/action-routes.js +8 -6
- package/dist/server/agent-chat/prompt-resources.js +117 -79
- package/dist/server/agent-chat/script-entries.js +15 -8
- package/dist/server/agent-chat-plugin.js +9 -4
- 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/agents-bundle.d.ts +3 -1
- package/dist/server/agents-bundle.js +4 -4
- package/dist/server/csrf.js +1 -0
- package/dist/server/dev-action-bridge.d.ts +2 -0
- package/dist/server/dev-action-bridge.js +34 -5
- package/dist/server/first-touch-route.d.ts +1 -1
- package/dist/server/http-response-telemetry.js +4 -0
- package/dist/server/loopback.js +3 -1
- package/dist/server/request-context.d.ts +26 -0
- package/dist/shared/app-state-routes.d.ts +7 -0
- package/dist/shared/app-state-routes.js +7 -0
- package/dist/shared/mcp-client-connections.d.ts +41 -0
- package/dist/shared/mcp-client-connections.js +18 -0
- package/dist/shared/mcp-connect-content.d.ts +18 -0
- package/dist/shared/mcp-connect-content.js +200 -172
- package/dist/shared/runtime-config.js +5 -1
- package/dist/templates/chat/.agents/skills/context-awareness/SKILL.md +13 -0
- package/dist/templates/default/.agents/skills/context-awareness/SKILL.md +13 -0
- package/dist/templates/workspace-core/.agents/skills/context-awareness/SKILL.md +13 -0
- package/dist/tracking/providers.d.ts +7 -0
- package/dist/tracking/providers.js +39 -17
- package/dist/triggers/condition-evaluator.d.ts +2 -0
- package/dist/triggers/condition-evaluator.js +93 -3
- package/dist/triggers/dispatcher.js +24 -6
- package/docs/AGENTS.md +16 -16
- package/docs/content/create-with-an-agent.mdx +10 -4
- package/docs/content/durable-resume.mdx +11 -2
- package/docs/content/toolkit-comments-review.mdx +54 -1
- package/package.json +5 -4
- package/src/templates/chat/.agents/skills/context-awareness/SKILL.md +13 -0
- package/src/templates/default/.agents/skills/context-awareness/SKILL.md +13 -0
- package/src/templates/workspace-core/.agents/skills/context-awareness/SKILL.md +13 -0
|
@@ -127,6 +127,10 @@ function addIssue(issues, issue, environment) {
|
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
129
|
export function buildRuntimeConfigPrompt(report, appName) {
|
|
130
|
+
// A healthy report has nothing to fix. A "fix this" prompt here would send
|
|
131
|
+
// an agent that reads the probe looking for problems that are not there.
|
|
132
|
+
if (report.issues.length === 0)
|
|
133
|
+
return "";
|
|
130
134
|
const appLine = appName ? `\nApp: ${appName}` : "";
|
|
131
135
|
const issueLines = report.issues
|
|
132
136
|
.map((issue) => `- ${issue.severity.toUpperCase()}: ${issue.title}. ${issue.message}`)
|
|
@@ -137,7 +141,7 @@ export function buildRuntimeConfigPrompt(report, appName) {
|
|
|
137
141
|
`Phase: ${report.phase}${appLine}`,
|
|
138
142
|
"",
|
|
139
143
|
"Detected issues:",
|
|
140
|
-
issueLines
|
|
144
|
+
issueLines,
|
|
141
145
|
"",
|
|
142
146
|
"Inspect the app's agent-native.json or agent-native.config.ts (including compatibility aliases), its deployment configuration, and the framework defaults. Make the smallest safe fix. Keep secrets out of source control. Do not print secret values in your response. Tell me exactly which deploy settings still need to be set, then verify the production build and runtime health check.",
|
|
143
147
|
].join("\n");
|
|
@@ -246,6 +246,19 @@ of the local route unless a matching `view` route or explicit `publicPath`
|
|
|
246
246
|
identifies the mounted URL. Carry the mounted form separately in `publicPath`;
|
|
247
247
|
never infer that `path` is mounted from its prefix alone.
|
|
248
248
|
|
|
249
|
+
**Calls from no tab.** An MCP tool call by a signed-in user carries no tab ID,
|
|
250
|
+
so the tab-scoped helpers (`readAppState("navigation")`,
|
|
251
|
+
`readAppStateForCurrentTab`, `writeAppStateForCurrentTab`) act on the tab of
|
|
252
|
+
this app the user used last within 12 hours. `useAgentRouteState` records a
|
|
253
|
+
tab's use when it gains focus or changes route; an app with its own navigation
|
|
254
|
+
sync calls `useBrowserTabUse(route)` from
|
|
255
|
+
`@agent-native/core/client/navigation`. The call doesn't become that tab:
|
|
256
|
+
`getCurrentRequestBrowserTabId()` stays null, and checks that must come from
|
|
257
|
+
the open editor keep requiring it. When no tab was used recently, `navigation`
|
|
258
|
+
reads return null rather than the account-wide row, writes keep going to the
|
|
259
|
+
account-wide row, and the MCP result says no tab was found. To pick a tab the
|
|
260
|
+
same way in your own code, use `resolveCurrentRequestStateTab()`.
|
|
261
|
+
|
|
249
262
|
## Jitter Prevention
|
|
250
263
|
|
|
251
264
|
Agent-side application-state writes are tagged with
|
|
@@ -246,6 +246,19 @@ of the local route unless a matching `view` route or explicit `publicPath`
|
|
|
246
246
|
identifies the mounted URL. Carry the mounted form separately in `publicPath`;
|
|
247
247
|
never infer that `path` is mounted from its prefix alone.
|
|
248
248
|
|
|
249
|
+
**Calls from no tab.** An MCP tool call by a signed-in user carries no tab ID,
|
|
250
|
+
so the tab-scoped helpers (`readAppState("navigation")`,
|
|
251
|
+
`readAppStateForCurrentTab`, `writeAppStateForCurrentTab`) act on the tab of
|
|
252
|
+
this app the user used last within 12 hours. `useAgentRouteState` records a
|
|
253
|
+
tab's use when it gains focus or changes route; an app with its own navigation
|
|
254
|
+
sync calls `useBrowserTabUse(route)` from
|
|
255
|
+
`@agent-native/core/client/navigation`. The call doesn't become that tab:
|
|
256
|
+
`getCurrentRequestBrowserTabId()` stays null, and checks that must come from
|
|
257
|
+
the open editor keep requiring it. When no tab was used recently, `navigation`
|
|
258
|
+
reads return null rather than the account-wide row, writes keep going to the
|
|
259
|
+
account-wide row, and the MCP result says no tab was found. To pick a tab the
|
|
260
|
+
same way in your own code, use `resolveCurrentRequestStateTab()`.
|
|
261
|
+
|
|
249
262
|
## Jitter Prevention
|
|
250
263
|
|
|
251
264
|
Agent-side application-state writes are tagged with
|
|
@@ -246,6 +246,19 @@ of the local route unless a matching `view` route or explicit `publicPath`
|
|
|
246
246
|
identifies the mounted URL. Carry the mounted form separately in `publicPath`;
|
|
247
247
|
never infer that `path` is mounted from its prefix alone.
|
|
248
248
|
|
|
249
|
+
**Calls from no tab.** An MCP tool call by a signed-in user carries no tab ID,
|
|
250
|
+
so the tab-scoped helpers (`readAppState("navigation")`,
|
|
251
|
+
`readAppStateForCurrentTab`, `writeAppStateForCurrentTab`) act on the tab of
|
|
252
|
+
this app the user used last within 12 hours. `useAgentRouteState` records a
|
|
253
|
+
tab's use when it gains focus or changes route; an app with its own navigation
|
|
254
|
+
sync calls `useBrowserTabUse(route)` from
|
|
255
|
+
`@agent-native/core/client/navigation`. The call doesn't become that tab:
|
|
256
|
+
`getCurrentRequestBrowserTabId()` stays null, and checks that must come from
|
|
257
|
+
the open editor keep requiring it. When no tab was used recently, `navigation`
|
|
258
|
+
reads return null rather than the account-wide row, writes keep going to the
|
|
259
|
+
account-wide row, and the MCP result says no tab was found. To pick a tab the
|
|
260
|
+
same way in your own code, use `resolveCurrentRequestStateTab()`.
|
|
261
|
+
|
|
249
262
|
## Jitter Prevention
|
|
250
263
|
|
|
251
264
|
Agent-side application-state writes are tagged with
|
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
export declare const AGENT_NATIVE_ANALYTICS_PROVIDER = "agent-native-analytics";
|
|
2
|
+
/**
|
|
3
|
+
* The end-of-request flush. On serverless, the caller hands this to the
|
|
4
|
+
* host's waitUntil, so sends still under way outlive the reply. Long-running
|
|
5
|
+
* servers don't freeze, and a flush per request would end their 10-second
|
|
6
|
+
* batching, so there it resolves at once.
|
|
7
|
+
*/
|
|
8
|
+
export declare function flushTrackingSendsForResponse(): Promise<void>;
|
|
2
9
|
export declare function sendPostHogEvent(name: string, properties: Record<string, unknown>, distinctId: string): boolean;
|
|
3
10
|
export declare function registerBuiltinProviders(): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { withoutWriteObservation } from "../action-frame.js";
|
|
2
2
|
import { getAppConfig } from "../app-config/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { platformWaitUntil } from "../server/background-work.js";
|
|
4
|
+
import { getRequestContext, getRequestRunContext, } from "../server/request-context.js";
|
|
4
5
|
import { isTestIdentity } from "../server/test-identity.js";
|
|
5
6
|
import { resolveLaneEndpoint } from "../shared/environment-lanes.js";
|
|
6
7
|
import { reshapeTrackedExceptionProperties } from "./posthog-exception.js";
|
|
@@ -32,12 +33,26 @@ function getTimer() {
|
|
|
32
33
|
function setTimer(t) {
|
|
33
34
|
globalThis[TIMER_KEY] = t;
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* A serverless host can freeze the function once the reply is out, cutting
|
|
38
|
+
* off a send still under way. Inside a request, the host's waitUntil keeps the
|
|
39
|
+
* function up until the send ends. Actions reach it without the request event:
|
|
40
|
+
* Cloudflare's through the request's run context, Netlify's through its store.
|
|
41
|
+
*/
|
|
42
|
+
function keepAlive(sending) {
|
|
43
|
+
try {
|
|
44
|
+
(getRequestRunContext()?.waitUntil ?? platformWaitUntil())?.(sending);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// coercion-ok: the send is already under way; a host refusing to wait for it only risks that event.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
35
50
|
function enqueue(url, body, headers, options) {
|
|
36
51
|
const queue = getQueue();
|
|
37
52
|
queue.push({ url, body, headers });
|
|
38
53
|
const flushImmediately = options?.flushImmediately ?? isServerlessRuntime();
|
|
39
54
|
if (flushImmediately || queue.length >= MAX_BATCH_SIZE) {
|
|
40
|
-
|
|
55
|
+
keepAlive(drainQueue());
|
|
41
56
|
}
|
|
42
57
|
else if (!getTimer()) {
|
|
43
58
|
const timer = setTimeout(() => {
|
|
@@ -72,6 +87,22 @@ function drainQueue() {
|
|
|
72
87
|
// hold the request's background lifetime until those sends settle.
|
|
73
88
|
return Promise.all([...inFlight]);
|
|
74
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Sends what is queued, then waits for every send still under way, including
|
|
92
|
+
* the ones a serverless runtime started the moment their event was tracked.
|
|
93
|
+
*/
|
|
94
|
+
function flushSends() {
|
|
95
|
+
return drainQueue().then(() => undefined);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The end-of-request flush. On serverless, the caller hands this to the
|
|
99
|
+
* host's waitUntil, so sends still under way outlive the reply. Long-running
|
|
100
|
+
* servers don't freeze, and a flush per request would end their 10-second
|
|
101
|
+
* batching, so there it resolves at once.
|
|
102
|
+
*/
|
|
103
|
+
export function flushTrackingSendsForResponse() {
|
|
104
|
+
return isServerlessRuntime() ? flushSends() : Promise.resolve();
|
|
105
|
+
}
|
|
75
106
|
function isLocalhostUrl(value) {
|
|
76
107
|
if (!value || !value.trim())
|
|
77
108
|
return false;
|
|
@@ -122,6 +153,7 @@ function shouldSkipAgentNativeAnalyticsForLocalhost() {
|
|
|
122
153
|
function isServerlessRuntime() {
|
|
123
154
|
return Boolean(process.env.NETLIFY ||
|
|
124
155
|
process.env.VERCEL ||
|
|
156
|
+
process.env.CF_PAGES ||
|
|
125
157
|
process.env.AWS_LAMBDA_FUNCTION_NAME ||
|
|
126
158
|
process.env.AWS_EXECUTION_ENV ||
|
|
127
159
|
process.env.LAMBDA_TASK_ROOT ||
|
|
@@ -202,9 +234,7 @@ function createPostHogProvider(apiKey, host, errorTracking) {
|
|
|
202
234
|
properties: { $set: traits },
|
|
203
235
|
}));
|
|
204
236
|
},
|
|
205
|
-
flush:
|
|
206
|
-
return drainQueue().then(() => undefined);
|
|
207
|
-
},
|
|
237
|
+
flush: flushSends,
|
|
208
238
|
};
|
|
209
239
|
}
|
|
210
240
|
export function sendPostHogEvent(name, properties, distinctId) {
|
|
@@ -258,9 +288,7 @@ function createMixpanelProvider(token) {
|
|
|
258
288
|
};
|
|
259
289
|
enqueue("https://api.mixpanel.com/engage", JSON.stringify([data]));
|
|
260
290
|
},
|
|
261
|
-
flush:
|
|
262
|
-
return drainQueue().then(() => undefined);
|
|
263
|
-
},
|
|
291
|
+
flush: flushSends,
|
|
264
292
|
};
|
|
265
293
|
}
|
|
266
294
|
function stripExceptionContextForAmplitude(properties) {
|
|
@@ -307,9 +335,7 @@ function createAmplitudeProvider(apiKey) {
|
|
|
307
335
|
};
|
|
308
336
|
enqueue("https://api2.amplitude.com/2/httpapi", JSON.stringify(data));
|
|
309
337
|
},
|
|
310
|
-
flush:
|
|
311
|
-
return drainQueue().then(() => undefined);
|
|
312
|
-
},
|
|
338
|
+
flush: flushSends,
|
|
313
339
|
};
|
|
314
340
|
}
|
|
315
341
|
function createWebhookProvider(url, authHeader) {
|
|
@@ -334,9 +360,7 @@ function createWebhookProvider(url, authHeader) {
|
|
|
334
360
|
timestamp: new Date().toISOString(),
|
|
335
361
|
}), extra);
|
|
336
362
|
},
|
|
337
|
-
flush:
|
|
338
|
-
return drainQueue().then(() => undefined);
|
|
339
|
-
},
|
|
363
|
+
flush: flushSends,
|
|
340
364
|
};
|
|
341
365
|
}
|
|
342
366
|
function createAgentNativeAnalyticsProvider(publicKey, endpoint) {
|
|
@@ -364,9 +388,7 @@ function createAgentNativeAnalyticsProvider(publicKey, endpoint) {
|
|
|
364
388
|
timestamp: new Date().toISOString(),
|
|
365
389
|
}), undefined, { flushImmediately });
|
|
366
390
|
},
|
|
367
|
-
flush:
|
|
368
|
-
return drainQueue().then(() => undefined);
|
|
369
|
-
},
|
|
391
|
+
flush: flushSends,
|
|
370
392
|
};
|
|
371
393
|
}
|
|
372
394
|
let _registered = false;
|
|
@@ -32,5 +32,7 @@ export declare function evaluateCondition(condition: string | undefined, payload
|
|
|
32
32
|
signal?: AbortSignal;
|
|
33
33
|
engine?: AgentEngine;
|
|
34
34
|
resolvedModel?: string;
|
|
35
|
+
/** Usage label for the check's model call, such as the automation's own. */
|
|
36
|
+
usageLabel?: string;
|
|
35
37
|
}): Promise<boolean>;
|
|
36
38
|
export declare function __clearConditionCache(): void;
|
|
@@ -27,6 +27,12 @@ import { getStoredModelForEngine, normalizeModelForEngine, resolveEngine, } from
|
|
|
27
27
|
import { createTtlCache } from "../shared/ttl-cache.js";
|
|
28
28
|
const CONDITION_EVAL_VERSION = "v4";
|
|
29
29
|
const CONDITION_EVALUATION_TIMEOUT_MS = 15_000;
|
|
30
|
+
// How long a check cut off by its deadline may take to unwind its aborted
|
|
31
|
+
// stream, so the usage it reported is saved before the caller moves on.
|
|
32
|
+
const ABORTED_CHECK_UNWIND_MS = 1_000;
|
|
33
|
+
// How long the caller waits for the check's usage write. A write still
|
|
34
|
+
// running after this finishes on its own; it never rejects.
|
|
35
|
+
const USAGE_WRITE_WAIT_MS = 5_000;
|
|
30
36
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
31
37
|
const MAX_CACHE_SIZE = 500;
|
|
32
38
|
const _cache = createTtlCache({
|
|
@@ -73,6 +79,9 @@ export async function evaluateCondition(condition, payload, identity, options =
|
|
|
73
79
|
const controller = new AbortController();
|
|
74
80
|
let timeoutId;
|
|
75
81
|
let removeAbortListener;
|
|
82
|
+
// Saving what the check spent stays outside the deadline, so a slow write
|
|
83
|
+
// never turns an answer the model already gave into a timeout.
|
|
84
|
+
const usageWrites = [];
|
|
76
85
|
const evaluate = async () => {
|
|
77
86
|
let engine;
|
|
78
87
|
let model;
|
|
@@ -115,7 +124,12 @@ export async function evaluateCondition(condition, payload, identity, options =
|
|
|
115
124
|
return cached;
|
|
116
125
|
}
|
|
117
126
|
// A classification is an answer, not a write of the call that emitted the event.
|
|
118
|
-
const result = await withoutWriteObservation(() => callClassifier(condition, payload, engine, model, controller.signal
|
|
127
|
+
const result = await withoutWriteObservation(() => callClassifier(condition, payload, engine, model, controller.signal, {
|
|
128
|
+
ownerEmail: identity.userEmail,
|
|
129
|
+
...(identity.orgId === undefined ? {} : { orgId: identity.orgId }),
|
|
130
|
+
...(identity.appId === undefined ? {} : { app: identity.appId }),
|
|
131
|
+
label: options.usageLabel ?? "automation-condition",
|
|
132
|
+
}, usageWrites));
|
|
119
133
|
if (controller.signal.aborted) {
|
|
120
134
|
throw new Error(options.signal?.aborted
|
|
121
135
|
? "Condition evaluation aborted."
|
|
@@ -141,10 +155,11 @@ export async function evaluateCondition(condition, payload, identity, options =
|
|
|
141
155
|
removeAbortListener = () => options.signal.removeEventListener("abort", onAbort);
|
|
142
156
|
})
|
|
143
157
|
: null;
|
|
158
|
+
const evaluation = evaluate();
|
|
144
159
|
let result;
|
|
145
160
|
try {
|
|
146
161
|
result = await Promise.race([
|
|
147
|
-
|
|
162
|
+
evaluation,
|
|
148
163
|
timeout,
|
|
149
164
|
...(aborted ? [aborted] : []),
|
|
150
165
|
]);
|
|
@@ -153,10 +168,54 @@ export async function evaluateCondition(condition, payload, identity, options =
|
|
|
153
168
|
if (timeoutId)
|
|
154
169
|
clearTimeout(timeoutId);
|
|
155
170
|
removeAbortListener?.();
|
|
171
|
+
// A check cut off by the deadline queues its usage write only once its
|
|
172
|
+
// aborted stream unwinds, so give it a moment before collecting writes.
|
|
173
|
+
await settledWithin(evaluation, ABORTED_CHECK_UNWIND_MS);
|
|
174
|
+
await settledWithin(Promise.all(usageWrites), USAGE_WRITE_WAIT_MS);
|
|
156
175
|
}
|
|
157
176
|
return result;
|
|
158
177
|
}
|
|
159
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Resolves when `promise` settles or `ms` passes, whichever is first. The
|
|
180
|
+
* timer is always cleared, so it needs no Node-only `unref` (Workers timers
|
|
181
|
+
* are plain numbers).
|
|
182
|
+
*/
|
|
183
|
+
async function settledWithin(promise, ms) {
|
|
184
|
+
let timer;
|
|
185
|
+
try {
|
|
186
|
+
await Promise.race([
|
|
187
|
+
promise.then(() => undefined, () => undefined),
|
|
188
|
+
new Promise((resolve) => {
|
|
189
|
+
timer = setTimeout(resolve, ms);
|
|
190
|
+
}),
|
|
191
|
+
]);
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Save what the check spent, as every other model call does, so usage
|
|
199
|
+
* dashboards and spend totals include it on whichever key paid for it.
|
|
200
|
+
* Never rejects, so callers can leave it running.
|
|
201
|
+
*/
|
|
202
|
+
async function recordClassifierUsage(scope, usage) {
|
|
203
|
+
if (usage.inputTokens <= 0 &&
|
|
204
|
+
usage.outputTokens <= 0 &&
|
|
205
|
+
!usage.cacheReadTokens &&
|
|
206
|
+
!usage.cacheWriteTokens &&
|
|
207
|
+
usage.builderCreditsUsed == null) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const { recordUsage } = await import("../usage/store.js");
|
|
212
|
+
await recordUsage({ ...scope, ...usage });
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
console.error("[triggers] Failed to persist condition check usage:", error);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function callClassifier(condition, payload, engine, model, signal, usageScope, usageWrites) {
|
|
160
219
|
let payloadStr;
|
|
161
220
|
try {
|
|
162
221
|
payloadStr = JSON.stringify(payload, null, 2);
|
|
@@ -181,6 +240,13 @@ Condition: "${condition}"
|
|
|
181
240
|
Does the event payload satisfy the condition above? Respond with ONLY "yes" or "no".`;
|
|
182
241
|
let text = "";
|
|
183
242
|
let streamErrorMessage;
|
|
243
|
+
const usage = {
|
|
244
|
+
inputTokens: 0,
|
|
245
|
+
outputTokens: 0,
|
|
246
|
+
cacheReadTokens: 0,
|
|
247
|
+
cacheWriteTokens: 0,
|
|
248
|
+
builderCreditsUsed: undefined,
|
|
249
|
+
};
|
|
184
250
|
try {
|
|
185
251
|
const stream = engine.stream({
|
|
186
252
|
model,
|
|
@@ -196,6 +262,16 @@ Does the event payload satisfy the condition above? Respond with ONLY "yes" or "
|
|
|
196
262
|
if (event.type === "text-delta") {
|
|
197
263
|
text += event.text;
|
|
198
264
|
}
|
|
265
|
+
else if (event.type === "usage") {
|
|
266
|
+
usage.inputTokens += event.inputTokens;
|
|
267
|
+
usage.outputTokens += event.outputTokens;
|
|
268
|
+
usage.cacheReadTokens += event.cacheReadTokens ?? 0;
|
|
269
|
+
usage.cacheWriteTokens += event.cacheWriteTokens ?? 0;
|
|
270
|
+
if (event.builderCreditsUsed !== undefined) {
|
|
271
|
+
usage.builderCreditsUsed =
|
|
272
|
+
(usage.builderCreditsUsed ?? 0) + event.builderCreditsUsed;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
199
275
|
else if (event.type === "stop" && event.reason === "error") {
|
|
200
276
|
streamErrorMessage = event.error ?? "model error";
|
|
201
277
|
}
|
|
@@ -209,6 +285,20 @@ Does the event payload satisfy the condition above? Respond with ONLY "yes" or "
|
|
|
209
285
|
? `Condition evaluation failed: ${err.message}`
|
|
210
286
|
: "Condition evaluation failed: unknown error");
|
|
211
287
|
}
|
|
288
|
+
finally {
|
|
289
|
+
// A check that failed or timed out after the model answered still spent.
|
|
290
|
+
usageWrites.push(recordClassifierUsage(usageScope, {
|
|
291
|
+
inputTokens: usage.inputTokens,
|
|
292
|
+
outputTokens: usage.outputTokens,
|
|
293
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
294
|
+
cacheWriteTokens: usage.cacheWriteTokens,
|
|
295
|
+
...(usage.builderCreditsUsed === undefined
|
|
296
|
+
? {}
|
|
297
|
+
: { builderCreditsUsed: usage.builderCreditsUsed }),
|
|
298
|
+
engineName: engine.name,
|
|
299
|
+
model,
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
212
302
|
if (streamErrorMessage) {
|
|
213
303
|
throw new Error(`Condition evaluation failed: ${streamErrorMessage}`);
|
|
214
304
|
}
|
|
@@ -2,7 +2,7 @@ import { automationMatchesEventOwner, resolveAutomationExecutionIdentity, } from
|
|
|
2
2
|
import { isProductionServerlessFunctionRuntime } from "../db/client.js";
|
|
3
3
|
import { subscribeAll, unsubscribe } from "../event-bus/index.js";
|
|
4
4
|
import { countAutomationCredentialState, trackAutomationPaused, } from "../jobs/automation-events.js";
|
|
5
|
-
import { applyAutomationFailure, classifyAutomationFailure, CLEAR_FAILURE_STATE, CONFIG_INVALID_ERROR_CODE, OWNER_MISSING_ERROR_CODE, pauseNow, } from "../jobs/automation-outcome.js";
|
|
5
|
+
import { applyAutomationFailure, classifyAutomationFailure, CLEAR_FAILURE_STATE, CONFIG_INVALID_ERROR_CODE, isTransientAutomationFailureCode, OWNER_MISSING_ERROR_CODE, pauseNow, } from "../jobs/automation-outcome.js";
|
|
6
6
|
import { BackgroundAutomationRunError, checkBackgroundAutomationCredentials, isBackgroundAutomationRunActive, runBackgroundAutomation, validateAutomationRunIdentity, } from "../jobs/background-automation-runner.js";
|
|
7
7
|
import { buildJobResourceContent, jobBelongsToApp, parseJobResource, patchJobFrontmatterFields, } from "../jobs/frontmatter.js";
|
|
8
8
|
import { registerRecurringSweepHandler, } from "../jobs/sweep-hooks.js";
|
|
@@ -121,6 +121,13 @@ Execute the following automation instructions, and only these:
|
|
|
121
121
|
|
|
122
122
|
${input.body}`;
|
|
123
123
|
}
|
|
124
|
+
function automationNameOf(resource) {
|
|
125
|
+
return resource.path.replace(/^jobs\//, "").replace(/\.md$/, "");
|
|
126
|
+
}
|
|
127
|
+
/** One label for a run and its condition check, so both count as its spend. */
|
|
128
|
+
function automationUsageLabel(resource) {
|
|
129
|
+
return `automation:${automationNameOf(resource)}`;
|
|
130
|
+
}
|
|
124
131
|
async function recordTriggerSkip(resource, status, reason, errorCode) {
|
|
125
132
|
await recordTriggerExecutionOutcome(resource, {
|
|
126
133
|
lastCheck: new Date().toISOString(),
|
|
@@ -193,7 +200,7 @@ failed) {
|
|
|
193
200
|
countAutomationCredentialState(failed.failure.code);
|
|
194
201
|
if (failed && pausedAfter !== undefined) {
|
|
195
202
|
trackAutomationPaused({
|
|
196
|
-
name: resource
|
|
203
|
+
name: automationNameOf(resource),
|
|
197
204
|
failure: failed.failure,
|
|
198
205
|
consecutiveFailures: pausedAfter,
|
|
199
206
|
surface: failed.pauseImmediately ? "preflight" : "trigger",
|
|
@@ -979,7 +986,13 @@ async function dispatchQueuedAutomationEvent(queued, deps, hardDeadlineAt) {
|
|
|
979
986
|
: {}),
|
|
980
987
|
}, deps, meta.model);
|
|
981
988
|
if (!credentialCheck.ok) {
|
|
982
|
-
await recordTriggerExecutionOutcome(resource, { lastCheck: new Date().toISOString() }, { failure: credentialCheck.failure });
|
|
989
|
+
await recordTriggerExecutionOutcome(resource, { lastCheck: new Date().toISOString() }, { failure: credentialCheck.failure, eventId: queued.eventId });
|
|
990
|
+
// An unreadable credential store or a provider outage clears by itself,
|
|
991
|
+
// so the event stays queued, as it does when an unconditional run fails.
|
|
992
|
+
// A missing or rejected key does not, so the event ends here.
|
|
993
|
+
if (isTransientAutomationFailureCode(credentialCheck.failure.code)) {
|
|
994
|
+
throw new BackgroundAutomationRunError(credentialCheck.failure.message, credentialCheck.failure.code);
|
|
995
|
+
}
|
|
983
996
|
return "completed";
|
|
984
997
|
}
|
|
985
998
|
classifierEngine = credentialCheck.engine;
|
|
@@ -1004,6 +1017,7 @@ async function dispatchQueuedAutomationEvent(queued, deps, hardDeadlineAt) {
|
|
|
1004
1017
|
deadlineAt: hardDeadlineAt,
|
|
1005
1018
|
engine: classifierEngine,
|
|
1006
1019
|
resolvedModel: classifierModel,
|
|
1020
|
+
usageLabel: automationUsageLabel(resource),
|
|
1007
1021
|
}));
|
|
1008
1022
|
}
|
|
1009
1023
|
catch (error) {
|
|
@@ -1099,7 +1113,11 @@ export async function dispatchAutomationWebhookTask(task) {
|
|
|
1099
1113
|
userEmail: identity.userEmail,
|
|
1100
1114
|
orgId: identity.orgId,
|
|
1101
1115
|
appId: deps.appId,
|
|
1102
|
-
}, {
|
|
1116
|
+
}, {
|
|
1117
|
+
engine: classifierEngine,
|
|
1118
|
+
resolvedModel: classifierModel,
|
|
1119
|
+
usageLabel: automationUsageLabel(resource),
|
|
1120
|
+
}));
|
|
1103
1121
|
}
|
|
1104
1122
|
catch (err) {
|
|
1105
1123
|
const reason = err instanceof Error ? err.message : "Condition evaluation failed";
|
|
@@ -1136,7 +1154,7 @@ export async function dispatchAutomationWebhookTask(task) {
|
|
|
1136
1154
|
async function dispatchAgentic(resource, payload, eventMeta, identity, hardDeadlineAt) {
|
|
1137
1155
|
if (!_deps)
|
|
1138
1156
|
return false;
|
|
1139
|
-
const triggerName = resource
|
|
1157
|
+
const triggerName = automationNameOf(resource);
|
|
1140
1158
|
const now = new Date();
|
|
1141
1159
|
const jobUserEmail = identity.userEmail;
|
|
1142
1160
|
const jobOrgId = identity.orgId;
|
|
@@ -1249,7 +1267,7 @@ async function dispatchAgentic(resource, payload, eventMeta, identity, hardDeadl
|
|
|
1249
1267
|
}),
|
|
1250
1268
|
threadTitle: `Trigger: ${triggerName} — ${now.toLocaleDateString()}`,
|
|
1251
1269
|
runIdPrefix: `automation-${triggerName}`,
|
|
1252
|
-
usageLabel:
|
|
1270
|
+
usageLabel: automationUsageLabel(resource),
|
|
1253
1271
|
usageRefId: eventMeta.eventId,
|
|
1254
1272
|
eventId: eventMeta.eventId,
|
|
1255
1273
|
requestContext,
|
package/docs/AGENTS.md
CHANGED
|
@@ -66,22 +66,22 @@ preserve public action, state, auth, and agent-chat runtime contracts.
|
|
|
66
66
|
|
|
67
67
|
## What To Read First
|
|
68
68
|
|
|
69
|
-
| Task
|
|
70
|
-
|
|
|
71
|
-
| Define or call app operations
|
|
72
|
-
| Add PostgreSQL data, schema, or access rules | `content/server-database.mdx`, `content/security.
|
|
73
|
-
| Keep the UI and agent in sync
|
|
74
|
-
| Localize UI copy and language choices
|
|
75
|
-
| Build headless or chat-first apps
|
|
76
|
-
| Add automations or scheduled work
|
|
77
|
-
| Compose apps or call sibling agents
|
|
78
|
-
| Expose tools to external agents
|
|
79
|
-
| Add integrations, setup, or secrets
|
|
80
|
-
| Build extensions or custom widgets
|
|
81
|
-
| Deploy or configure hosting
|
|
82
|
-
| Write agent instructions or skills
|
|
83
|
-
| Plan a breaking package API change
|
|
84
|
-
| Migrate the Core chat API to AgentKit
|
|
69
|
+
| Task | Start with |
|
|
70
|
+
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
71
|
+
| Define or call app operations | `content/actions-overview.mdx`, `content/client-overview.mdx` |
|
|
72
|
+
| Add PostgreSQL data, schema, or access rules | `content/server-database.mdx`, `content/security.mdx`, `content/sharing.mdx` |
|
|
73
|
+
| Keep the UI and agent in sync | `content/context-awareness.mdx`, `content/client-overview.mdx` |
|
|
74
|
+
| Localize UI copy and language choices | `content/internationalization.mdx`, `content/client-overview.mdx` |
|
|
75
|
+
| Build headless or chat-first apps | `content/pure-agent-apps.mdx`, `content/agent-surfaces.mdx`, `content/using-your-agent.mdx` |
|
|
76
|
+
| Add automations or scheduled work | `content/automations.mdx`, `content/recurring-jobs.mdx` |
|
|
77
|
+
| Compose apps or call sibling agents | `content/a2a-protocol.mdx`, `content/multi-app-workspace.mdx`, `content/agent-resources.mdx` |
|
|
78
|
+
| Expose tools to external agents | `content/external-agents.mdx`, `content/mcp-protocol.mdx`, `content/mcp-apps.mdx`, `content/mcp-clients.mdx` |
|
|
79
|
+
| Add integrations, setup, or secrets | `content/onboarding.mdx`, `content/workspace-connections.mdx`, `content/security.mdx` |
|
|
80
|
+
| Build extensions or custom widgets | `content/extensions.mdx`, `content/agent-web-surfaces.mdx` |
|
|
81
|
+
| Deploy or configure hosting | `content/deployment.mdx`, `content/server-overview.mdx` |
|
|
82
|
+
| Write agent instructions or skills | `content/skills-guide.mdx`, `content/writing-agent-instructions.mdx` |
|
|
83
|
+
| Plan a breaking package API change | `migrations/README.md` |
|
|
84
|
+
| Migrate the Core chat API to AgentKit | `migrations/agentkit-chat.md`, `content/native-chat-ui.mdx` |
|
|
85
85
|
|
|
86
86
|
## Rules
|
|
87
87
|
|
|
@@ -36,10 +36,14 @@ it. Use that name wherever a step below says APP_NAME.
|
|
|
36
36
|
4. Read APP_NAME/AGENTS.md so you know how the app is laid out.
|
|
37
37
|
5. Start the app with `pnpm dev --no-open` and keep it running in the background.
|
|
38
38
|
6. Read the local URL that `pnpm dev` prints. Check that it responds,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
with `curl "<url>/_agent-native/ping?configuration=1"`. The reply
|
|
40
|
+
should include `"message":"pong"` and a configuration status of
|
|
41
|
+
`"ok"`. Do not open a browser and do not use headless browser
|
|
42
|
+
tools. I will open the app myself.
|
|
43
|
+
7. In the APP_NAME folder, run `pnpm action hello`. It runs the
|
|
44
|
+
app's sample action and should print "Hello, world!".
|
|
45
|
+
8. Show me the result. Give me the local URL and what both checks
|
|
46
|
+
printed. Do not just say that setup is complete.
|
|
43
47
|
|
|
44
48
|
Rules:
|
|
45
49
|
|
|
@@ -57,6 +61,8 @@ A good run ends with these results, and you can check each one yourself:
|
|
|
57
61
|
- A new app folder, named `my-app` unless you changed it, that contains `actions/`, `app/`, and an `AGENTS.md` file.
|
|
58
62
|
- `pnpm dev` running without errors.
|
|
59
63
|
- A local URL, usually `http://localhost:8080`, that the agent has checked responds.
|
|
64
|
+
- A reply from `<url>/_agent-native/ping?configuration=1` with a configuration status of `ok`.
|
|
65
|
+
- `pnpm action hello` printing "Hello, world!".
|
|
60
66
|
|
|
61
67
|
Then open the local URL in your own browser. On the Welcome screen, choose **Continue as local dev**. The Get started page appears.
|
|
62
68
|
|
|
@@ -83,7 +83,7 @@ When a run resumes (soft timeout, gateway timeout, or any resumable transport er
|
|
|
83
83
|
- which tool calls **already completed** (with short results) so it reuses them and does **not** re-run them, and
|
|
84
84
|
- which tool calls were **interrupted with unknown outcome** so it verifies state before assuming success or failure.
|
|
85
85
|
|
|
86
|
-
When the journal is empty (a turn with no tool activity, or a clean continuation), nothing extra is appended and resume behavior is byte-for-byte what it was before. The
|
|
86
|
+
When the journal is empty (a turn with no tool activity, or a clean continuation), nothing extra is appended and resume behavior is byte-for-byte what it was before. The journal itself is required, though: if the ledger can't be read, even after one retry, the resumed run stops before the model runs, with `tool_call_journal_unreadable`, and asks you to send the request again. Without the ledger it can't tell which steps already finished.
|
|
87
87
|
|
|
88
88
|
## Layer 2: tool-layer hard-block {#hard-block}
|
|
89
89
|
|
|
@@ -103,13 +103,22 @@ Key properties:
|
|
|
103
103
|
The hard-block stops only an **exact repeat**: the same write tool with the same input, after an earlier attempt in the same turn finished and recorded its result. It does not rule out every duplicate email, charge, or ticket:
|
|
104
104
|
|
|
105
105
|
- **A reworded retry isn't matched.** If the model changes any argument, such as the email body, the call no longer matches a completed call, so the hard-block doesn't stop it.
|
|
106
|
-
- **
|
|
106
|
+
- **After a crash mid-send, a repeat is held back only for the rest of the turn.** If the process dies after the side effect lands but before its result is recorded, the call counts as interrupted, and the hard-block covers only completed calls. The resumed run holds back that write tool instead, as [Writes with an unknown outcome](#unknown-outcome) describes. A new message starts a new turn, where nothing holds it back.
|
|
107
107
|
- **A crashed run can resume in the same turn through either of two separate paths.** One is a [durable background run](/docs/durable-background-runs). The other is a later chunk of a normal foreground turn using the [foreground self-chain](/docs/durable-background-runs#foreground-self-chain). Any other crashed run ends with an error. Choosing **Continue** on it resumes the same turn, so both layers apply.
|
|
108
108
|
|
|
109
109
|
An action whose side effect must never repeat needs its own idempotency check. The hard-block never applies to read-only actions.
|
|
110
110
|
|
|
111
111
|
</Callout>
|
|
112
112
|
|
|
113
|
+
## Writes with an unknown outcome {#unknown-outcome}
|
|
114
|
+
|
|
115
|
+
A write's outcome is unknown when it started but no result was recorded, when it timed out, or when it failed after it began writing without saying whether anything was saved. Running it again could apply the change twice, so for the rest of the turn the loop holds back that tool:
|
|
116
|
+
|
|
117
|
+
- **The same call again.** If the model repeats the call with the same arguments, the loop first checks whether that call's own result was recorded after all, and returns it if so. Otherwise the call doesn't run: the run stops with `write_tool_outcome_unknown` and asks you to check what was saved, then reply "continue".
|
|
118
|
+
- **Any other call to that tool.** A call with different arguments, such as a reworded email, doesn't run either. The model is told that the earlier call may already have applied, and to check the current state with a read and tell you what it found. The turn goes on, unless the earlier call can't be identified because it recorded no arguments to compare or was left out of the replayed history. Then any call to that tool could be a repeat, so it stops the run the way the same call does.
|
|
119
|
+
|
|
120
|
+
An action marked `retrySafe` is exempt, because running it again can't duplicate or corrupt data: it validates before it writes, or it sets an end state, such as replacing a record by ID. The model can correct its arguments and try again. Read-only actions aren't held back unless a write ran inside one.
|
|
121
|
+
|
|
113
122
|
## Automatic continuation after a time limit {#auto-continue}
|
|
114
123
|
|
|
115
124
|
When the server stops a turn at its run time limit and nothing server-side picks it up, the chat continues it on its own, in the **same turn**. The resumed run gets the thread's tool calls and results plus the journal note above, so finished steps are reused, and an identical repeat of a write — including a cross-app `call-agent` delegation, whose idempotency key is derived from the turn — returns the recorded result instead of being sent again. The chat shows a _Resuming_ status, and Stop keeps working.
|
|
@@ -75,6 +75,58 @@ If the resource is already registered with the Sharing Kit, the review actions
|
|
|
75
75
|
reuse that access model. Add a review registration when the app needs a custom
|
|
76
76
|
access resolver or wants to make the resource type explicit in setup code.
|
|
77
77
|
|
|
78
|
+
## App Rules {#app-rules}
|
|
79
|
+
|
|
80
|
+
A registration can also set how comments behave on its resource:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
registerReviewableResource({
|
|
84
|
+
type: "deck",
|
|
85
|
+
comments: { minimumRole: "viewer", unmentionedRoute: "human" },
|
|
86
|
+
anchorKinds: {
|
|
87
|
+
"slide-point": { version: 2, parse: parseSlidePoint },
|
|
88
|
+
},
|
|
89
|
+
async onCommentWrite(event, tx) {
|
|
90
|
+
await tx.execute(/* keep the deck's comment count in step */);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- `comments.minimumRole` is the lowest role that may comment, reply, react and
|
|
96
|
+
edit or delete their own comments (default `commenter`). Suggesting needs it
|
|
97
|
+
too, and never less than `commenter`.
|
|
98
|
+
`comments.unmentionedRoute` sends a new thread with no mention to the agent
|
|
99
|
+
(the default) or to people.
|
|
100
|
+
- `anchorKinds` types the anchor. Once a resource declares any, every anchored
|
|
101
|
+
comment names its `anchorKind`, and `parse(anchor, version)` checks it and
|
|
102
|
+
returns what's stored, so an older version can be read forward. A comment is
|
|
103
|
+
written at the kind's current `version` unless it gives an older one. A
|
|
104
|
+
suggestion's thread uses the kit's own `review-suggestion` kind, which
|
|
105
|
+
every resource accepts and only the kit writes: the comment actions refuse
|
|
106
|
+
it.
|
|
107
|
+
- `onCommentWrite` runs inside the comment's own database transaction for
|
|
108
|
+
`created`, `edited`, `anchor-moved`, `resolved`, `reopened` and `deleted`.
|
|
109
|
+
Resolving a thread that is already resolved, or reopening an open one,
|
|
110
|
+
fires no event, though a new resolution note is still saved. Throwing
|
|
111
|
+
refuses the write. Use the `tx` handle for database work there. A database
|
|
112
|
+
client without transactions refuses a hooked write.
|
|
113
|
+
|
|
114
|
+
Every comment also carries a `surfaceId` (the slide, page or screen a thread
|
|
115
|
+
sits on; `list-review-comments` filters by it) and a `revision` that goes up
|
|
116
|
+
when it changes, including a thread's routing on its first comment. Pass the
|
|
117
|
+
revision you read as `expectedRevision` to
|
|
118
|
+
`update-review-comment` or `update-review-comment-anchor`, and a newer one
|
|
119
|
+
refuses the write with `revision_conflict`. `list-review-comments` pages oldest
|
|
120
|
+
first: pass back its `nextCursor` as `cursor` to read past the `limit` of 500.
|
|
121
|
+
A cursor resumes only the list it came from, at any `limit`; with another
|
|
122
|
+
resource or filter it's refused with `invalid_cursor`. A newest-first list isn't
|
|
123
|
+
paged and returns no `nextCursor`.
|
|
124
|
+
|
|
125
|
+
`linkReviewComment()` ties a comment to an ID in another system, such as a
|
|
126
|
+
Notion discussion, so an import skips what it already copied.
|
|
127
|
+
`findReviewCommentLink()` and `listReviewCommentLinks()` read those links back.
|
|
128
|
+
An ID links to one comment, and a link outlives the comment's deletion.
|
|
129
|
+
|
|
78
130
|
## UI Usage {#ui-usage}
|
|
79
131
|
|
|
80
132
|
```tsx
|
|
@@ -96,7 +148,8 @@ export function DocumentReview({ documentId }: { documentId: string }) {
|
|
|
96
148
|
|
|
97
149
|
The `anchor` field is intentionally `unknown`: editors, canvases, diagrams, and
|
|
98
150
|
documents can store the shape they need while still using the shared review
|
|
99
|
-
actions and thread UI.
|
|
151
|
+
actions and thread UI. Declare [anchor kinds](#app-rules) to have the kit check
|
|
152
|
+
that shape for you.
|
|
100
153
|
|
|
101
154
|
## UX Standard {#ux-standard}
|
|
102
155
|
|