@agent-native/core 0.157.6 → 0.157.8
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/README.md +1 -1
- package/corpus/templates/analytics/agent-native.config.ts +5 -0
- package/corpus/templates/analytics/netlify.toml +1 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +23 -5
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
- package/corpus/templates/assets/netlify.toml +3 -0
- package/corpus/templates/brain/netlify.toml +3 -0
- package/corpus/templates/calendar/agent-native.config.ts +5 -0
- package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -1
- package/corpus/templates/calendar/netlify.toml +3 -0
- package/corpus/templates/chat/agent-native.config.ts +5 -0
- package/corpus/templates/chat/netlify.toml +3 -0
- package/corpus/templates/clips/actions/lib/meeting-content.ts +28 -0
- package/corpus/templates/clips/actions/list-meetings.ts +33 -11
- package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
- package/corpus/templates/clips/app/lib/meeting-history-query.ts +11 -0
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +47 -21
- package/corpus/templates/clips/netlify.toml +4 -0
- package/corpus/templates/content/netlify.toml +3 -0
- package/corpus/templates/design/app/i18n-data.ts +2 -0
- package/corpus/templates/design/netlify.toml +3 -0
- package/corpus/templates/dispatch/netlify.toml +3 -0
- package/corpus/templates/factory/netlify.toml +3 -0
- package/corpus/templates/forms/netlify.toml +3 -0
- package/corpus/templates/macros/netlify.toml +3 -0
- package/corpus/templates/mail/agent-native.config.ts +5 -0
- package/corpus/templates/mail/netlify.toml +3 -0
- package/corpus/templates/plan/netlify.toml +3 -0
- package/corpus/templates/slides/netlify.toml +3 -0
- package/dist/agent/harness/acp-adapter.js +17 -2
- package/dist/agent/harness/ai-sdk-adapter.js +14 -3
- package/dist/agent/harness/background.js +10 -5
- package/dist/agent/harness/hosted.d.ts +20 -0
- package/dist/agent/harness/hosted.js +100 -0
- package/dist/agent/harness/runner.js +100 -77
- package/dist/agent/observational-memory/observer.js +12 -0
- package/dist/agent/production-agent.d.ts +3 -0
- package/dist/agent/production-agent.js +74 -4
- package/dist/agent/thread-data-builder.d.ts +12 -0
- package/dist/agent/thread-data-builder.js +23 -0
- package/dist/agent/types.d.ts +6 -0
- package/dist/client/AgentPanel.d.ts +5 -1
- package/dist/client/AgentPanel.js +113 -17
- package/dist/client/AgentTaskCard.d.ts +2 -2
- package/dist/client/AgentTaskCard.js +8 -22
- package/dist/client/AssistantChat.d.ts +8 -0
- package/dist/client/AssistantChat.js +106 -59
- package/dist/client/MultiTabAssistantChat.js +50 -6
- package/dist/client/NewWorkspaceAppFlow.js +7 -1
- package/dist/client/agent-chat-adapter.d.ts +6 -0
- package/dist/client/agent-chat-adapter.js +36 -8
- package/dist/client/app-chat-sidebar.d.ts +5 -0
- package/dist/client/app-chat-sidebar.js +53 -9
- package/dist/client/chat/composer-draft.d.ts +4 -0
- package/dist/client/chat/composer-draft.js +63 -0
- package/dist/client/chat-first/apps-rail.js +10 -1
- package/dist/client/chat-first/primary-nav.js +10 -5
- package/dist/client/frame-protocol.d.ts +1 -0
- package/dist/client/hooks/index.d.ts +1 -1
- package/dist/client/hooks/index.js +1 -1
- package/dist/client/i18n.js +5 -2
- package/dist/client/progress/RunsTray.js +129 -16
- package/dist/client/settings/SettingsRow.js +1 -1
- package/dist/client/use-chat-threads.js +11 -6
- package/dist/config.d.ts +11 -0
- package/dist/config.js +52 -0
- package/dist/extensions/actions.js +22 -1
- package/dist/hosted-harness/actions/get-hosted-harness-config.d.ts +2 -0
- package/dist/hosted-harness/actions/get-hosted-harness-config.js +13 -0
- package/dist/hosted-harness/actions/set-hosted-harness-enabled.d.ts +4 -0
- package/dist/hosted-harness/actions/set-hosted-harness-enabled.js +32 -0
- package/dist/localization/default-messages.d.ts +4 -1
- package/dist/localization/default-messages.js +5 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/server/action-discovery.js +12 -0
- package/dist/server/agent-chat-plugin.js +24 -4
- package/dist/server/agent-teams.d.ts +20 -12
- package/dist/server/agent-teams.js +69 -35
- package/dist/server/hosted-harness-policy.d.ts +17 -0
- package/dist/server/hosted-harness-policy.js +38 -0
- package/dist/server/poll.d.ts +4 -4
- package/dist/server/poll.js +45 -13
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/request-context.d.ts +4 -0
- package/dist/styles/agent-native.css +39 -0
- package/dist/templates/chat/agent-native.config.ts +5 -0
- package/dist/templates/chat/netlify.toml +3 -0
- package/dist/vite/action-types-plugin.js +8 -0
- package/package.json +5 -3
- package/src/templates/chat/agent-native.config.ts +5 -0
- package/src/templates/chat/netlify.toml +3 -0
|
@@ -98,7 +98,7 @@ function toAgentHarnessBackgroundRun(session) {
|
|
|
98
98
|
},
|
|
99
99
|
title: `${session.harnessName} harness`,
|
|
100
100
|
subtitle: session.status === "running" ? "Running" : undefined,
|
|
101
|
-
status: backgroundStatus(session.status),
|
|
101
|
+
status: backgroundStatus(session.status, Boolean(session.pendingApproval)),
|
|
102
102
|
phase: session.status,
|
|
103
103
|
createdAt,
|
|
104
104
|
updatedAt,
|
|
@@ -218,14 +218,17 @@ function parseRunEvent(row) {
|
|
|
218
218
|
return null;
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
|
-
function backgroundStatus(status) {
|
|
221
|
+
function backgroundStatus(status, needsApproval) {
|
|
222
222
|
if (status === "running")
|
|
223
223
|
return "running";
|
|
224
224
|
if (status === "errored")
|
|
225
225
|
return "errored";
|
|
226
|
-
if (
|
|
226
|
+
if (needsApproval)
|
|
227
|
+
return "needs-approval";
|
|
228
|
+
if (status === "idle")
|
|
229
|
+
return "paused";
|
|
230
|
+
if (status === "stopped" || status === "destroyed")
|
|
227
231
|
return "completed";
|
|
228
|
-
}
|
|
229
232
|
return "unknown";
|
|
230
233
|
}
|
|
231
234
|
async function sendAgentHarnessBackgroundFollowUp(input, scope) {
|
|
@@ -253,7 +256,9 @@ async function controlAgentHarnessBackgroundRun(input, scope) {
|
|
|
253
256
|
}
|
|
254
257
|
if (input.command === "approve" || input.command === "deny") {
|
|
255
258
|
const session = await getAgentHarnessSessionByRunId(input.runId);
|
|
256
|
-
if (!session ||
|
|
259
|
+
if (!session ||
|
|
260
|
+
session.ownerEmail !== scope.ownerEmail ||
|
|
261
|
+
(scope.orgId !== undefined && session.orgId !== scope.orgId)) {
|
|
257
262
|
return missingHarnessRunResult(input.runId);
|
|
258
263
|
}
|
|
259
264
|
const pending = session.pendingApproval;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentNativeHarnessRuntime } from "../../config.js";
|
|
2
|
+
export type HostedHarnessRuntime = AgentNativeHarnessRuntime;
|
|
3
|
+
export declare const HOSTED_HARNESS_ENV_KEY: "AGENT_NATIVE_HOSTED_HARNESS";
|
|
4
|
+
export declare const HOSTED_HARNESS_ORG_SETTING_KEY: "agent-harness.enabled";
|
|
5
|
+
export declare const HOSTED_HARNESS_RUNTIME_ORDER: readonly HostedHarnessRuntime[];
|
|
6
|
+
export declare const HOSTED_HARNESS_AGENT_DESCRIPTIONS: Record<HostedHarnessRuntime, string>;
|
|
7
|
+
export declare const HOSTED_HARNESS_AGENT_LABELS: Record<HostedHarnessRuntime, string>;
|
|
8
|
+
export declare const HOSTED_HARNESS_BLOCKED_TOOL_NAMES: Set<string>;
|
|
9
|
+
export declare function isHostedHarnessRuntime(value: unknown): value is HostedHarnessRuntime;
|
|
10
|
+
export declare function normalizeHostedHarnessRuntime(value: unknown): HostedHarnessRuntime | undefined;
|
|
11
|
+
export declare function normalizeHostedHarnessRuntimes(value: unknown): HostedHarnessRuntime[];
|
|
12
|
+
export declare function isHostedHarnessConfigured(value: unknown): boolean;
|
|
13
|
+
export declare function hostedHarnessAgentOption(runtime: HostedHarnessRuntime): {
|
|
14
|
+
id: AgentNativeHarnessRuntime;
|
|
15
|
+
label: string;
|
|
16
|
+
description: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function isHostedHarnessBlockedToolName(name: string): boolean;
|
|
19
|
+
export declare function filterHostedHarnessToolNames(names: readonly string[]): string[];
|
|
20
|
+
export declare function hostedHarnessSystemPrompt(runtime: HostedHarnessRuntime): string;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export const HOSTED_HARNESS_ENV_KEY = "AGENT_NATIVE_HOSTED_HARNESS";
|
|
2
|
+
export const HOSTED_HARNESS_ORG_SETTING_KEY = "agent-harness.enabled";
|
|
3
|
+
export const HOSTED_HARNESS_RUNTIME_ORDER = [
|
|
4
|
+
"claude-code",
|
|
5
|
+
"codex",
|
|
6
|
+
"pi",
|
|
7
|
+
"opencode",
|
|
8
|
+
];
|
|
9
|
+
export const HOSTED_HARNESS_AGENT_DESCRIPTIONS = {
|
|
10
|
+
"claude-code": "Production mode is tools-only: no repository, shell, or code editing. Use the Electron app for full coding workflows.",
|
|
11
|
+
codex: "Production mode is tools-only: no repository, shell, or code editing. Use the Electron app for full coding workflows.",
|
|
12
|
+
pi: "Production mode is tools-only: no repository, shell, or code editing. Use the Electron app for full coding workflows.",
|
|
13
|
+
opencode: "Production mode is tools-only: no repository, shell, or code editing. Use the Electron app for full coding workflows.",
|
|
14
|
+
};
|
|
15
|
+
export const HOSTED_HARNESS_AGENT_LABELS = {
|
|
16
|
+
"claude-code": "Claude Code",
|
|
17
|
+
codex: "Codex",
|
|
18
|
+
pi: "Pi",
|
|
19
|
+
opencode: "OpenCode",
|
|
20
|
+
};
|
|
21
|
+
export const HOSTED_HARNESS_BLOCKED_TOOL_NAMES = new Set([
|
|
22
|
+
"bash",
|
|
23
|
+
"edit",
|
|
24
|
+
"read",
|
|
25
|
+
"write",
|
|
26
|
+
"run-code",
|
|
27
|
+
"tool-orchestration",
|
|
28
|
+
"get-code-execution",
|
|
29
|
+
"read-file",
|
|
30
|
+
"write-file",
|
|
31
|
+
"read-local-file",
|
|
32
|
+
"write-local-file",
|
|
33
|
+
"apply-source-edit",
|
|
34
|
+
"update-file",
|
|
35
|
+
"delete-file",
|
|
36
|
+
"github-repo-list-files",
|
|
37
|
+
"github-repo-read-file",
|
|
38
|
+
"github-repo-search-code",
|
|
39
|
+
"github-repo-write-file",
|
|
40
|
+
"github-repo-delete-file",
|
|
41
|
+
"save-data-program",
|
|
42
|
+
"preview-data-program",
|
|
43
|
+
"run-data-program",
|
|
44
|
+
"list-data-programs",
|
|
45
|
+
"get-data-program",
|
|
46
|
+
"delete-data-program",
|
|
47
|
+
"workspace-files",
|
|
48
|
+
"show-workspace-file",
|
|
49
|
+
"read-workspace-file",
|
|
50
|
+
"write-workspace-file",
|
|
51
|
+
"delete-workspace-file",
|
|
52
|
+
"grep-workspace-file",
|
|
53
|
+
"connect-builder",
|
|
54
|
+
]);
|
|
55
|
+
export function isHostedHarnessRuntime(value) {
|
|
56
|
+
return (value === "claude-code" ||
|
|
57
|
+
value === "codex" ||
|
|
58
|
+
value === "pi" ||
|
|
59
|
+
value === "opencode");
|
|
60
|
+
}
|
|
61
|
+
export function normalizeHostedHarnessRuntime(value) {
|
|
62
|
+
if (value === "claude")
|
|
63
|
+
return "claude-code";
|
|
64
|
+
return isHostedHarnessRuntime(value) ? value : undefined;
|
|
65
|
+
}
|
|
66
|
+
export function normalizeHostedHarnessRuntimes(value) {
|
|
67
|
+
if (!Array.isArray(value))
|
|
68
|
+
return [...HOSTED_HARNESS_RUNTIME_ORDER];
|
|
69
|
+
const runtimes = value
|
|
70
|
+
.map((runtime) => normalizeHostedHarnessRuntime(runtime))
|
|
71
|
+
.filter((runtime) => runtime !== undefined);
|
|
72
|
+
return [...new Set(runtimes)];
|
|
73
|
+
}
|
|
74
|
+
export function isHostedHarnessConfigured(value) {
|
|
75
|
+
return (value === true ||
|
|
76
|
+
(!!value && typeof value === "object" && !Array.isArray(value)));
|
|
77
|
+
}
|
|
78
|
+
export function hostedHarnessAgentOption(runtime) {
|
|
79
|
+
return {
|
|
80
|
+
id: runtime,
|
|
81
|
+
label: HOSTED_HARNESS_AGENT_LABELS[runtime],
|
|
82
|
+
description: HOSTED_HARNESS_AGENT_DESCRIPTIONS[runtime],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function isHostedHarnessBlockedToolName(name) {
|
|
86
|
+
return (HOSTED_HARNESS_BLOCKED_TOOL_NAMES.has(name) ||
|
|
87
|
+
name.startsWith("workspace-file-") ||
|
|
88
|
+
name.startsWith("data-program-"));
|
|
89
|
+
}
|
|
90
|
+
export function filterHostedHarnessToolNames(names) {
|
|
91
|
+
return names.filter((name) => !isHostedHarnessBlockedToolName(name));
|
|
92
|
+
}
|
|
93
|
+
export function hostedHarnessSystemPrompt(runtime) {
|
|
94
|
+
return `<hosted-tools-only-harness runtime="${runtime}">
|
|
95
|
+
You are running as a hosted ${HOSTED_HARNESS_AGENT_LABELS[runtime]} inside a production app.
|
|
96
|
+
This is a tools-only mode. You have no repository, shell, filesystem, code-editing, or code-execution tools.
|
|
97
|
+
Use the app's typed actions and SQL-backed resources for mail, calendar, analytics, and working state.
|
|
98
|
+
For full coding work, tell the user to use the Agent Native Electron app.
|
|
99
|
+
</hosted-tools-only-harness>`;
|
|
100
|
+
}
|
|
@@ -6,92 +6,115 @@ export function startAgentHarnessRun(opts) {
|
|
|
6
6
|
let harnessSession = opts.session;
|
|
7
7
|
const detachOnComplete = opts.detachOnComplete !== false;
|
|
8
8
|
return startRun(opts.runId, opts.threadId, async (send, signal) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
await
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
await
|
|
54
|
-
|
|
55
|
-
pendingApproval
|
|
56
|
-
|
|
9
|
+
let storedSessionId;
|
|
10
|
+
let keepLiveSession = false;
|
|
11
|
+
try {
|
|
12
|
+
send({
|
|
13
|
+
type: "activity",
|
|
14
|
+
label: `Starting ${opts.adapter.label}`,
|
|
15
|
+
tool: "harness",
|
|
16
|
+
});
|
|
17
|
+
harnessSession ??= await opts.adapter.createSession({
|
|
18
|
+
...(opts.createSession ?? {}),
|
|
19
|
+
threadId: opts.threadId,
|
|
20
|
+
runId: opts.runId,
|
|
21
|
+
ownerEmail: opts.ownerEmail ?? opts.createSession?.ownerEmail ?? null,
|
|
22
|
+
orgId: opts.orgId ?? opts.createSession?.orgId ?? null,
|
|
23
|
+
signal,
|
|
24
|
+
});
|
|
25
|
+
storedSessionId = opts.createSession?.sessionId ?? harnessSession.id;
|
|
26
|
+
await saveAgentHarnessSession({
|
|
27
|
+
id: storedSessionId,
|
|
28
|
+
harnessName: opts.adapter.name,
|
|
29
|
+
threadId: opts.threadId,
|
|
30
|
+
runId: opts.runId,
|
|
31
|
+
providerSessionId: harnessSession.id,
|
|
32
|
+
status: "running",
|
|
33
|
+
resumeState: opts.createSession?.resumeState,
|
|
34
|
+
ownerEmail: opts.ownerEmail ?? opts.createSession?.ownerEmail ?? null,
|
|
35
|
+
orgId: opts.orgId ?? opts.createSession?.orgId ?? null,
|
|
36
|
+
});
|
|
37
|
+
registerLiveAgentHarnessSession({
|
|
38
|
+
sessionId: storedSessionId,
|
|
39
|
+
adapter: opts.adapter,
|
|
40
|
+
session: harnessSession,
|
|
41
|
+
createSession: opts.createSession,
|
|
42
|
+
ownerEmail: opts.ownerEmail ?? opts.createSession?.ownerEmail ?? null,
|
|
43
|
+
orgId: opts.orgId ?? opts.createSession?.orgId ?? null,
|
|
44
|
+
});
|
|
45
|
+
const input = {
|
|
46
|
+
...opts.input,
|
|
47
|
+
abortSignal: signal,
|
|
48
|
+
};
|
|
49
|
+
let pendingApproval = null;
|
|
50
|
+
for await (const event of harnessSession.streamTurn(input)) {
|
|
51
|
+
if (signal.aborted)
|
|
52
|
+
break;
|
|
53
|
+
await opts.onHarnessEvent?.(event);
|
|
54
|
+
if (event.type === "approval-request") {
|
|
55
|
+
pendingApproval = event;
|
|
56
|
+
await updateAgentHarnessSession(storedSessionId, {
|
|
57
|
+
status: "idle",
|
|
58
|
+
pendingApproval: event,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (event.type === "error") {
|
|
62
|
+
throw new Error(event.error);
|
|
63
|
+
}
|
|
64
|
+
for (const chatEvent of agentHarnessEventToAgentChatEvents(event)) {
|
|
65
|
+
send(chatEvent);
|
|
66
|
+
}
|
|
57
67
|
}
|
|
58
|
-
if (
|
|
59
|
-
|
|
68
|
+
if (signal.aborted) {
|
|
69
|
+
await stopHarnessSession(harnessSession);
|
|
70
|
+
releaseLiveAgentHarnessSession(storedSessionId, harnessSession);
|
|
71
|
+
await markAgentHarnessSessionStopped(storedSessionId, "stopped");
|
|
72
|
+
return;
|
|
60
73
|
}
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
if (pendingApproval) {
|
|
75
|
+
const stored = await getAgentHarnessSession(storedSessionId);
|
|
76
|
+
const stillPending = stored?.pendingApproval &&
|
|
77
|
+
typeof stored.pendingApproval === "object" &&
|
|
78
|
+
"id" in stored.pendingApproval &&
|
|
79
|
+
stored.pendingApproval.id === pendingApproval.id;
|
|
80
|
+
if (stillPending) {
|
|
81
|
+
keepLiveSession = true;
|
|
82
|
+
await updateAgentHarnessSession(storedSessionId, {
|
|
83
|
+
status: "idle",
|
|
84
|
+
pendingApproval,
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let resumeState = opts.createSession?.resumeState;
|
|
90
|
+
if (detachOnComplete && harnessSession.detach) {
|
|
91
|
+
resumeState = await harnessSession.detach();
|
|
63
92
|
}
|
|
64
|
-
}
|
|
65
|
-
if (signal.aborted) {
|
|
66
|
-
await stopHarnessSession(harnessSession);
|
|
67
93
|
releaseLiveAgentHarnessSession(storedSessionId, harnessSession);
|
|
68
|
-
await
|
|
69
|
-
|
|
94
|
+
await updateAgentHarnessSession(storedSessionId, {
|
|
95
|
+
status: "idle",
|
|
96
|
+
resumeState,
|
|
97
|
+
pendingApproval: null,
|
|
98
|
+
});
|
|
70
99
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (stillPending) {
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (harnessSession && !keepLiveSession) {
|
|
102
|
+
await stopHarnessSession(harnessSession).catch(() => undefined);
|
|
103
|
+
}
|
|
104
|
+
if (storedSessionId) {
|
|
105
|
+
releaseLiveAgentHarnessSession(storedSessionId, harnessSession);
|
|
78
106
|
await updateAgentHarnessSession(storedSessionId, {
|
|
79
|
-
status: "
|
|
80
|
-
pendingApproval,
|
|
81
|
-
});
|
|
82
|
-
return;
|
|
107
|
+
status: "errored",
|
|
108
|
+
pendingApproval: null,
|
|
109
|
+
}).catch(() => undefined);
|
|
83
110
|
}
|
|
111
|
+
throw error;
|
|
84
112
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
113
|
+
finally {
|
|
114
|
+
if (!keepLiveSession && storedSessionId) {
|
|
115
|
+
releaseLiveAgentHarnessSession(storedSessionId, harnessSession);
|
|
116
|
+
}
|
|
88
117
|
}
|
|
89
|
-
releaseLiveAgentHarnessSession(storedSessionId, harnessSession);
|
|
90
|
-
await updateAgentHarnessSession(opts.createSession?.sessionId ?? harnessSession.id, {
|
|
91
|
-
status: "idle",
|
|
92
|
-
resumeState,
|
|
93
|
-
pendingApproval: null,
|
|
94
|
-
});
|
|
95
118
|
}, opts.onRunComplete, {
|
|
96
119
|
...(opts.runOptions ?? {}),
|
|
97
120
|
turnId: opts.turnId ?? opts.runOptions?.turnId,
|
|
@@ -31,6 +31,18 @@ export async function runObserver(options) {
|
|
|
31
31
|
threadId: options.threadId,
|
|
32
32
|
});
|
|
33
33
|
const startIndex = observedThrough + 1;
|
|
34
|
+
// The cursor is a position in whatever array the previous pass observed. The
|
|
35
|
+
// live agent loop passes the engine array, where each tool result is its own
|
|
36
|
+
// entry; other callers pass the store-derived array, which folds those into
|
|
37
|
+
// their tool-call parts and runs several times shorter. A cursor past the end
|
|
38
|
+
// of THIS array is a basis mismatch, not an up-to-date thread: the window can
|
|
39
|
+
// never become non-empty again, so the thread silently stops accruing memory.
|
|
40
|
+
// Measured in production — 17 of 19 threads carry a cursor above their stored
|
|
41
|
+
// message count, one at 57 against 9.
|
|
42
|
+
if (startIndex > options.messages.length) {
|
|
43
|
+
console.warn(`[observational-memory] thread ${options.threadId}: cursor ${observedThrough} is past the end of this ${options.messages.length}-message window, so nothing can be observed on this basis.`);
|
|
44
|
+
return { observed: false, unobservedTokens: 0 };
|
|
45
|
+
}
|
|
34
46
|
const unobserved = options.messages.slice(startIndex);
|
|
35
47
|
if (unobserved.length === 0) {
|
|
36
48
|
return { observed: false, unobservedTokens: 0 };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EventHandler as H3EventHandler } from "h3";
|
|
2
2
|
import type { Task } from "../a2a/types.js";
|
|
3
3
|
import { type ActionAutomationContext, type ActionCaller } from "../action.js";
|
|
4
|
+
import type { AgentNativeHarnessSetting } from "../config.js";
|
|
4
5
|
import { fireInternalDispatch } from "../server/self-dispatch.js";
|
|
5
6
|
import { type ReasoningEffort } from "../shared/reasoning-effort.js";
|
|
6
7
|
import { type ResolveEngineConfig } from "./engine/index.js";
|
|
@@ -299,6 +300,8 @@ export interface ProductionAgentOptions {
|
|
|
299
300
|
model?: string;
|
|
300
301
|
/** App/template id used for org-scoped per-app model defaults. */
|
|
301
302
|
appId?: string;
|
|
303
|
+
/** Static app capability for the hosted tools-only harness picker. */
|
|
304
|
+
hostedHarnessConfig?: AgentNativeHarnessSetting;
|
|
302
305
|
/** Default effort for requests that do not supply an override. */
|
|
303
306
|
reasoningEffort?: ReasoningEffort;
|
|
304
307
|
/** Provider-specific options passed through to the engine */
|
|
@@ -16,6 +16,7 @@ import { completeRun as completeProgressRun, startRun as startProgressRun, updat
|
|
|
16
16
|
import { isRuntimeVisibleScope, parseSkillFrontmatter, } from "../server/agent-chat/skill-frontmatter.js";
|
|
17
17
|
import { canUseDeployCredentialFallbackForRequest, getProviderCredentialAuthFailure, readDeployCredentialEnv, } from "../server/credential-provider.js";
|
|
18
18
|
import { readBody } from "../server/h3-helpers.js";
|
|
19
|
+
import { resolveHostedHarnessPolicy } from "../server/hosted-harness-policy.js";
|
|
19
20
|
import { assertRequestActionSurfaceIsolation, getRequestRunContext, ensureRequestRunContext, getRequestContext, getRequestOrgId, getRequestUserEmail, runWithRequestContext, } from "../server/request-context.js";
|
|
20
21
|
import { fireInternalDispatch } from "../server/self-dispatch.js";
|
|
21
22
|
import { isReasoningEffort, normalizeReasoningEffortForRequest, stepDownReasoningEffort, } from "../shared/reasoning-effort.js";
|
|
@@ -33,6 +34,7 @@ import { PROVIDER_TO_ENV } from "./engine/provider-env-vars.js";
|
|
|
33
34
|
import { loadPriorTurnToolCallJournal } from "./engine/tool-call-journal-seed.js";
|
|
34
35
|
import { backfillEngineMessagesToolResults, stringifyToolUseInputForGateway, unmatchedToolResultReplayText, } from "./engine/translate-anthropic.js";
|
|
35
36
|
import { EngineError } from "./engine/types.js";
|
|
37
|
+
import { filterHostedHarnessToolNames, normalizeHostedHarnessRuntime, } from "./harness/hosted.js";
|
|
36
38
|
import { getDefaultMaxIterations, getDefaultMaxRunInputTokens, MAX_AGENT_MAX_ITERATIONS, MIN_AGENT_MAX_ITERATIONS, normalizeMaxIterations, normalizeMaxRunInputTokens, readAgentLoopSettings, } from "./loop-settings.js";
|
|
37
39
|
import { maybeCompactThread, buildObservationalContext, hasObservationalMemory, serializeObservationalMemoryBlock, } from "./observational-memory/index.js";
|
|
38
40
|
import { ProcessorChain, TripWire, toolCallsFromContent, } from "./processors.js";
|
|
@@ -5056,7 +5058,7 @@ export async function runAgentLoop(opts) {
|
|
|
5056
5058
|
// the user-visible turn.
|
|
5057
5059
|
if (opts.ownerEmail) {
|
|
5058
5060
|
const compactThreadId = opts.threadId;
|
|
5059
|
-
|
|
5061
|
+
const compaction = maybeCompactThread({
|
|
5060
5062
|
threadId: compactThreadId,
|
|
5061
5063
|
ownerEmail: opts.ownerEmail,
|
|
5062
5064
|
orgId: opts.orgId ?? null,
|
|
@@ -5064,6 +5066,17 @@ export async function runAgentLoop(opts) {
|
|
|
5064
5066
|
}).catch((err) => {
|
|
5065
5067
|
console.warn("[observational-memory] post-turn compaction skipped:", err instanceof Error ? err.message : String(err));
|
|
5066
5068
|
});
|
|
5069
|
+
// This pass has to finish a streaming model call before it writes, and
|
|
5070
|
+
// it starts after the turn's `done` event — on a host that freezes the
|
|
5071
|
+
// isolate once the response settles, an unregistered promise is simply
|
|
5072
|
+
// killed, so the thread never accrues the memory that would spare the
|
|
5073
|
+
// next turn. Hand it to the platform keep-alive where there is one;
|
|
5074
|
+
// long-lived hosts keep the existing fire-and-forget behavior.
|
|
5075
|
+
const waitUntil = getRequestRunContext()?.waitUntil;
|
|
5076
|
+
if (waitUntil)
|
|
5077
|
+
waitUntil(compaction);
|
|
5078
|
+
else
|
|
5079
|
+
void compaction;
|
|
5067
5080
|
}
|
|
5068
5081
|
}
|
|
5069
5082
|
}
|
|
@@ -6135,7 +6148,7 @@ export function createProductionAgentHandler(options) {
|
|
|
6135
6148
|
return { error: "Invalid request body" };
|
|
6136
6149
|
}
|
|
6137
6150
|
}
|
|
6138
|
-
const { message, history = [], structuredHistory, references = [], threadId, attachments, displayMessage, queuedMessageId, internalContinuation, turnId: requestTurnId, model: requestModel, engine: requestEngine, effort: requestEffort, browserTabId, scope, trackInRunsTray, } = body;
|
|
6151
|
+
const { message, history = [], structuredHistory, references = [], threadId, attachments, displayMessage, queuedMessageId, internalContinuation, turnId: requestTurnId, model: requestModel, engine: requestEngine, effort: requestEffort, browserTabId, scope, harness: requestHarness, trackInRunsTray, } = body;
|
|
6139
6152
|
setupMark("bodyParsed");
|
|
6140
6153
|
// Durable-background marker. Present ONLY when this handler was re-entered
|
|
6141
6154
|
// as the Netlify background worker via the `_process-run` self-dispatch
|
|
@@ -6258,7 +6271,36 @@ export function createProductionAgentHandler(options) {
|
|
|
6258
6271
|
requestAttachments = preparedRequest.attachments;
|
|
6259
6272
|
}
|
|
6260
6273
|
}
|
|
6261
|
-
const
|
|
6274
|
+
const requestedHostedHarness = normalizeHostedHarnessRuntime(requestHarness?.runtime);
|
|
6275
|
+
if (requestHarness !== undefined && !requestedHostedHarness) {
|
|
6276
|
+
setResponseStatus(event, 400);
|
|
6277
|
+
return { error: "Unsupported hosted harness runtime" };
|
|
6278
|
+
}
|
|
6279
|
+
const hostedHarnessPolicy = requestedHostedHarness
|
|
6280
|
+
? await resolveHostedHarnessPolicy({
|
|
6281
|
+
config: options.hostedHarnessConfig,
|
|
6282
|
+
orgId: getRequestOrgId() ?? null,
|
|
6283
|
+
userEmail: ownerEmail,
|
|
6284
|
+
})
|
|
6285
|
+
: null;
|
|
6286
|
+
if (requestedHostedHarness &&
|
|
6287
|
+
(!hostedHarnessPolicy?.enabled ||
|
|
6288
|
+
!hostedHarnessPolicy.runtimes.includes(requestedHostedHarness))) {
|
|
6289
|
+
setResponseStatus(event, hostedHarnessPolicy?.configEnabled ? 403 : 404);
|
|
6290
|
+
return {
|
|
6291
|
+
error: hostedHarnessPolicy?.configEnabled
|
|
6292
|
+
? "Hosted harness is not enabled for this organization"
|
|
6293
|
+
: "Hosted harness is not configured for this app",
|
|
6294
|
+
};
|
|
6295
|
+
}
|
|
6296
|
+
const runContext = ensureRequestRunContext();
|
|
6297
|
+
if (runContext && requestedHostedHarness) {
|
|
6298
|
+
runContext.hostedHarnessRuntime = requestedHostedHarness;
|
|
6299
|
+
}
|
|
6300
|
+
let availableRequestActions = getRequestActions();
|
|
6301
|
+
if (requestedHostedHarness) {
|
|
6302
|
+
availableRequestActions = filterActionsByAllowedNames(availableRequestActions, filterHostedHarnessToolNames(Object.keys(availableRequestActions)));
|
|
6303
|
+
}
|
|
6262
6304
|
let surfacedRequestActions = availableRequestActions;
|
|
6263
6305
|
if (options.resolveActionSurface) {
|
|
6264
6306
|
const persistedSurface = isBackgroundWorker
|
|
@@ -6276,6 +6318,9 @@ export function createProductionAgentHandler(options) {
|
|
|
6276
6318
|
availableActionNames: Object.keys(availableRequestActions),
|
|
6277
6319
|
});
|
|
6278
6320
|
surfacedRequestActions = filterActionsByAllowedNames(availableRequestActions, surface.allowedActionNames);
|
|
6321
|
+
if (requestedHostedHarness) {
|
|
6322
|
+
surfacedRequestActions = filterActionsByAllowedNames(surfacedRequestActions, filterHostedHarnessToolNames(Object.keys(surfacedRequestActions)));
|
|
6323
|
+
}
|
|
6279
6324
|
const allowedNames = Object.keys(surfacedRequestActions);
|
|
6280
6325
|
const runCtx = ensureRequestRunContext();
|
|
6281
6326
|
if (runCtx)
|
|
@@ -6604,8 +6649,9 @@ export function createProductionAgentHandler(options) {
|
|
|
6604
6649
|
// is unrelated to the app's job (e.g. a voice-first macro tracker).
|
|
6605
6650
|
const filesContextThunk = () => (async () => {
|
|
6606
6651
|
let filesContext = "";
|
|
6607
|
-
if (options.skipFilesContext)
|
|
6652
|
+
if (options.skipFilesContext || requestedHostedHarness) {
|
|
6608
6653
|
return filesContext;
|
|
6654
|
+
}
|
|
6609
6655
|
if (history.length === 0) {
|
|
6610
6656
|
try {
|
|
6611
6657
|
const { resourceListAccessible, SHARED_OWNER, WORKSPACE_OWNER, resourceGet, } = await import("../resources/store.js");
|
|
@@ -6963,6 +7009,30 @@ export function createProductionAgentHandler(options) {
|
|
|
6963
7009
|
// Keep the body-derived messages — never drop the run.
|
|
6964
7010
|
}
|
|
6965
7011
|
}
|
|
7012
|
+
// A foreground turn on an existing thread that arrives with NO history is
|
|
7013
|
+
// amnesia, not a new conversation — the client trims history against a size
|
|
7014
|
+
// budget and one tool-heavy turn could zero it out, which reads downstream
|
|
7015
|
+
// as an ordinary first message. Recover what was said from the stored
|
|
7016
|
+
// thread. Runs only on the foreground path, before `onRunPrepared` persists
|
|
7017
|
+
// this turn, so the recovered window cannot contain the current message.
|
|
7018
|
+
if (!isBackgroundWorker &&
|
|
7019
|
+
!internalContinuation &&
|
|
7020
|
+
threadId &&
|
|
7021
|
+
historyMessages.length === 0) {
|
|
7022
|
+
try {
|
|
7023
|
+
const { getThread } = await import("../chat-threads/store.js");
|
|
7024
|
+
const { recoverThreadHistoryForRequest } = await import("./thread-data-builder.js");
|
|
7025
|
+
const recovered = recoverThreadHistoryForRequest((await getThread(threadId))?.threadData);
|
|
7026
|
+
if (recovered.length > 0)
|
|
7027
|
+
messages.unshift(...recovered);
|
|
7028
|
+
}
|
|
7029
|
+
catch (err) {
|
|
7030
|
+
// Never drop the run over this — but never let it pass for "the thread
|
|
7031
|
+
// had nothing to recover" either. That silence is the same failure this
|
|
7032
|
+
// block exists to fix, one layer down.
|
|
7033
|
+
console.warn(`[agent-chat] history recovery failed for thread ${threadId}; continuing with no prior context:`, err);
|
|
7034
|
+
}
|
|
7035
|
+
}
|
|
6966
7036
|
setupMark("depsThread");
|
|
6967
7037
|
// DIAGNOSTIC-ONLY: owner/thread resolution + runId/effectiveThreadId +
|
|
6968
7038
|
// chained-continuation thread fetch finished.
|
|
@@ -73,6 +73,18 @@ export declare function normalizeThreadRepository(repo: any): any;
|
|
|
73
73
|
* Originally inlined in `integrations/webhook-handler.ts`.
|
|
74
74
|
*/
|
|
75
75
|
export declare function threadDataToEngineMessages(threadData: string | Record<string, unknown> | null | undefined): EngineMessage[];
|
|
76
|
+
/**
|
|
77
|
+
* The trailing window of what was actually said in a thread, for a request that
|
|
78
|
+
* arrived carrying no history of its own. The client trims history against a
|
|
79
|
+
* size budget, so one tool-heavy turn can zero it out; without this floor the
|
|
80
|
+
* model re-derives answers it already gave and re-asks questions the user
|
|
81
|
+
* already answered. Contiguous and bounded on purpose — this restores the
|
|
82
|
+
* conversation, not the tool transcript.
|
|
83
|
+
*/
|
|
84
|
+
export declare function recoverThreadHistoryForRequest(threadData: string | Record<string, unknown> | null | undefined, limits?: {
|
|
85
|
+
maxMessages?: number;
|
|
86
|
+
maxChars?: number;
|
|
87
|
+
}): EngineMessage[];
|
|
76
88
|
/**
|
|
77
89
|
* Select the participant-visible delivery for integration turns while keeping
|
|
78
90
|
* a compact, trusted resource ledger available to the agent. Raw tool results
|
|
@@ -671,6 +671,29 @@ export function threadDataToEngineMessages(threadData) {
|
|
|
671
671
|
}
|
|
672
672
|
return messages;
|
|
673
673
|
}
|
|
674
|
+
const MAX_RECOVERED_HISTORY_MESSAGES = 12;
|
|
675
|
+
const MAX_RECOVERED_HISTORY_CHARS = 32_000;
|
|
676
|
+
function engineMessageTextLength(message) {
|
|
677
|
+
return message.content.reduce((total, part) => total + (part.type === "text" ? (part.text?.length ?? 0) : 0), 0);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* The trailing window of what was actually said in a thread, for a request that
|
|
681
|
+
* arrived carrying no history of its own. The client trims history against a
|
|
682
|
+
* size budget, so one tool-heavy turn can zero it out; without this floor the
|
|
683
|
+
* model re-derives answers it already gave and re-asks questions the user
|
|
684
|
+
* already answered. Contiguous and bounded on purpose — this restores the
|
|
685
|
+
* conversation, not the tool transcript.
|
|
686
|
+
*/
|
|
687
|
+
export function recoverThreadHistoryForRequest(threadData, limits) {
|
|
688
|
+
const maxMessages = limits?.maxMessages ?? MAX_RECOVERED_HISTORY_MESSAGES;
|
|
689
|
+
const maxChars = limits?.maxChars ?? MAX_RECOVERED_HISTORY_CHARS;
|
|
690
|
+
const window = threadDataToEngineMessages(threadData).slice(-maxMessages);
|
|
691
|
+
let total = window.reduce((sum, message) => sum + engineMessageTextLength(message), 0);
|
|
692
|
+
while (window.length > 1 && total > maxChars) {
|
|
693
|
+
total -= engineMessageTextLength(window.shift());
|
|
694
|
+
}
|
|
695
|
+
return window;
|
|
696
|
+
}
|
|
674
697
|
const MAX_INTEGRATION_ARTIFACTS_IN_CONTEXT = 12;
|
|
675
698
|
const MAX_INTEGRATION_ARTIFACT_FIELD_CHARS = 500;
|
|
676
699
|
function boundedString(value) {
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -129,6 +129,10 @@ export interface AgentChatScope {
|
|
|
129
129
|
id: string;
|
|
130
130
|
label?: string;
|
|
131
131
|
}
|
|
132
|
+
export interface AgentChatHarnessRequest {
|
|
133
|
+
/** Hosted tools-only runtime selected in the production app picker. */
|
|
134
|
+
runtime: "claude-code" | "codex" | "pi" | "opencode";
|
|
135
|
+
}
|
|
132
136
|
export interface AgentChatRequest {
|
|
133
137
|
message: string;
|
|
134
138
|
/** Stable identity of a durable queued message, used to reject replayed delivery. */
|
|
@@ -217,6 +221,8 @@ export interface AgentChatRequest {
|
|
|
217
221
|
browserTabId?: string;
|
|
218
222
|
/** Resource scope for this chat thread, e.g. the deck currently bound to the tab. */
|
|
219
223
|
scope?: AgentChatScope | null;
|
|
224
|
+
/** Optional hosted tools-only harness selection. */
|
|
225
|
+
harness?: AgentChatHarnessRequest;
|
|
220
226
|
/** When true, expose this chat turn as a user-visible run in RunsTray. */
|
|
221
227
|
trackInRunsTray?: boolean;
|
|
222
228
|
/**
|
|
@@ -175,6 +175,10 @@ export interface AgentSidebarProps {
|
|
|
175
175
|
imageModelMenu?: AssistantChatProps["imageModelMenu"];
|
|
176
176
|
/** Local or hosted agent runtimes shown above the model list. */
|
|
177
177
|
availableAgents?: AssistantChatProps["availableAgents"];
|
|
178
|
+
/** Host-provided model catalog used by native chat surfaces. */
|
|
179
|
+
availableModels?: AssistantChatProps["availableModels"];
|
|
180
|
+
/** Whether the host-provided model catalog is still loading. */
|
|
181
|
+
modelListLoading?: AssistantChatProps["modelListLoading"];
|
|
178
182
|
/** Selected agent runtime identifier. */
|
|
179
183
|
selectedAgent?: AssistantChatProps["selectedAgent"];
|
|
180
184
|
/** Callback when the user picks an agent runtime. */
|
|
@@ -246,7 +250,7 @@ export interface AgentSidebarProps {
|
|
|
246
250
|
* Wraps app content with a toggleable agent sidebar.
|
|
247
251
|
* Use AgentToggleButton in your header to open/close it.
|
|
248
252
|
*/
|
|
249
|
-
export declare function AgentSidebar({ children, emptyStateText, suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, selectedAgent, onAgentChange, onConnectLocalRuntime, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position, defaultOpen, animateMobile, animateDesktop, chatViewTransition, chatViewTransitionHandoff, storageKey, openStorageKey, apiUrl, agentChatSurface, showTabBar, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning, onFullscreenRequest, scope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding, }: AgentSidebarProps): React.JSX.Element;
|
|
253
|
+
export declare function AgentSidebar({ children, emptyStateText, suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position, defaultOpen, animateMobile, animateDesktop, chatViewTransition, chatViewTransitionHandoff, storageKey, openStorageKey, apiUrl, agentChatSurface, showTabBar, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning, onFullscreenRequest, scope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding, }: AgentSidebarProps): React.JSX.Element;
|
|
250
254
|
/**
|
|
251
255
|
* Focus the agent chat composer input.
|
|
252
256
|
* Opens the sidebar if closed, then focuses the text input.
|