@agent-native/core 0.157.5 → 0.157.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/agent-native.config.ts +5 -0
  3. package/corpus/templates/analytics/netlify.toml +1 -0
  4. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +23 -5
  5. package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
  6. package/corpus/templates/assets/netlify.toml +3 -0
  7. package/corpus/templates/brain/netlify.toml +3 -0
  8. package/corpus/templates/calendar/agent-native.config.ts +5 -0
  9. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -1
  10. package/corpus/templates/calendar/netlify.toml +3 -0
  11. package/corpus/templates/chat/agent-native.config.ts +5 -0
  12. package/corpus/templates/chat/netlify.toml +3 -0
  13. package/corpus/templates/clips/actions/create-meeting.ts +19 -1
  14. package/corpus/templates/clips/actions/lib/meeting-content.ts +28 -0
  15. package/corpus/templates/clips/actions/list-meetings.ts +33 -11
  16. package/corpus/templates/clips/app/components/meetings/bullet-link.tsx +48 -25
  17. package/corpus/templates/clips/app/components/meetings/canvas-editor.tsx +7 -6
  18. package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +388 -178
  19. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +7 -13
  20. package/corpus/templates/clips/app/components/transcript/transcript-segment-row.tsx +98 -0
  21. package/corpus/templates/clips/app/i18n/en-US.ts +5 -0
  22. package/corpus/templates/clips/app/lib/meeting-history-query.ts +11 -0
  23. package/corpus/templates/clips/app/lib/public-meeting.ts +4 -0
  24. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +49 -31
  25. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +47 -21
  26. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +15 -1
  27. package/corpus/templates/clips/netlify.toml +8 -4
  28. package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +10 -0
  29. package/corpus/templates/clips/shared/transcript-segments.ts +20 -2
  30. package/corpus/templates/content/netlify.toml +3 -0
  31. package/corpus/templates/design/app/i18n-data.ts +2 -0
  32. package/corpus/templates/design/netlify.toml +3 -0
  33. package/corpus/templates/dispatch/netlify.toml +3 -0
  34. package/corpus/templates/factory/netlify.toml +3 -0
  35. package/corpus/templates/forms/netlify.toml +3 -0
  36. package/corpus/templates/macros/netlify.toml +3 -0
  37. package/corpus/templates/mail/agent-native.config.ts +5 -0
  38. package/corpus/templates/mail/netlify.toml +3 -0
  39. package/corpus/templates/plan/netlify.toml +3 -0
  40. package/corpus/templates/slides/netlify.toml +3 -0
  41. package/dist/agent/harness/acp-adapter.js +17 -2
  42. package/dist/agent/harness/ai-sdk-adapter.js +14 -3
  43. package/dist/agent/harness/background.js +10 -5
  44. package/dist/agent/harness/hosted.d.ts +20 -0
  45. package/dist/agent/harness/hosted.js +100 -0
  46. package/dist/agent/harness/runner.js +100 -77
  47. package/dist/agent/observational-memory/observer.js +12 -0
  48. package/dist/agent/production-agent.d.ts +3 -0
  49. package/dist/agent/production-agent.js +74 -4
  50. package/dist/agent/thread-data-builder.d.ts +12 -0
  51. package/dist/agent/thread-data-builder.js +23 -0
  52. package/dist/agent/types.d.ts +6 -0
  53. package/dist/client/AgentPanel.d.ts +5 -1
  54. package/dist/client/AgentPanel.js +113 -17
  55. package/dist/client/AgentTaskCard.d.ts +2 -2
  56. package/dist/client/AgentTaskCard.js +8 -22
  57. package/dist/client/AssistantChat.d.ts +8 -0
  58. package/dist/client/AssistantChat.js +106 -59
  59. package/dist/client/MultiTabAssistantChat.js +50 -6
  60. package/dist/client/NewWorkspaceAppFlow.js +7 -1
  61. package/dist/client/agent-chat-adapter.d.ts +6 -0
  62. package/dist/client/agent-chat-adapter.js +36 -8
  63. package/dist/client/app-chat-sidebar.d.ts +5 -0
  64. package/dist/client/app-chat-sidebar.js +53 -9
  65. package/dist/client/chat/composer-draft.d.ts +4 -0
  66. package/dist/client/chat/composer-draft.js +63 -0
  67. package/dist/client/chat-first/apps-rail.js +10 -1
  68. package/dist/client/chat-first/primary-nav.js +10 -5
  69. package/dist/client/frame-protocol.d.ts +1 -0
  70. package/dist/client/hooks/index.d.ts +1 -1
  71. package/dist/client/hooks/index.js +1 -1
  72. package/dist/client/i18n.js +5 -2
  73. package/dist/client/progress/RunsTray.js +129 -16
  74. package/dist/client/settings/SettingsRow.js +1 -1
  75. package/dist/client/use-chat-threads.js +11 -6
  76. package/dist/collab/routes.d.ts +1 -1
  77. package/dist/collab/struct-routes.d.ts +1 -1
  78. package/dist/config.d.ts +11 -0
  79. package/dist/config.js +52 -0
  80. package/dist/deploy/build.js +28 -11
  81. package/dist/extensions/actions.js +22 -1
  82. package/dist/hosted-harness/actions/get-hosted-harness-config.d.ts +2 -0
  83. package/dist/hosted-harness/actions/get-hosted-harness-config.js +13 -0
  84. package/dist/hosted-harness/actions/set-hosted-harness-enabled.d.ts +4 -0
  85. package/dist/hosted-harness/actions/set-hosted-harness-enabled.js +32 -0
  86. package/dist/localization/default-messages.d.ts +4 -1
  87. package/dist/localization/default-messages.js +5 -2
  88. package/dist/observability/routes.d.ts +5 -5
  89. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  90. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  91. package/dist/resources/handlers.d.ts +1 -1
  92. package/dist/secrets/routes.d.ts +9 -9
  93. package/dist/server/action-discovery.js +12 -0
  94. package/dist/server/agent-chat-plugin.js +24 -4
  95. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  96. package/dist/server/agent-teams.d.ts +20 -12
  97. package/dist/server/agent-teams.js +69 -35
  98. package/dist/server/hosted-harness-policy.d.ts +17 -0
  99. package/dist/server/hosted-harness-policy.js +38 -0
  100. package/dist/server/realtime-token.d.ts +1 -1
  101. package/dist/server/request-context.d.ts +4 -0
  102. package/dist/server/transcribe-voice.d.ts +1 -1
  103. package/dist/styles/agent-native.css +39 -0
  104. package/dist/templates/chat/agent-native.config.ts +5 -0
  105. package/dist/templates/chat/netlify.toml +3 -0
  106. package/dist/vite/action-types-plugin.js +8 -0
  107. package/package.json +4 -2
  108. package/src/templates/chat/agent-native.config.ts +5 -0
  109. package/src/templates/chat/netlify.toml +3 -0
@@ -13,6 +13,9 @@ NPM_CONFIG_PRODUCTION = "false"
13
13
  # function, including workspace-resource updates delegated by sibling apps.
14
14
  AGENT_CHAT_DURABLE_BACKGROUND = "true"
15
15
 
16
+ [context.production.environment]
17
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
18
+
16
19
  # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
17
20
  # The framework does not impose a default agent-run timeout; if a template sets
18
21
  # AGENT_RUN_SOFT_TIMEOUT_MS, keep it below this function timeout so the hidden
@@ -8,3 +8,6 @@ GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
8
8
  GTM_CONTAINER_ID = "GTM-N3WSTXZ"
9
9
  NITRO_PRESET = "netlify"
10
10
  NPM_CONFIG_PRODUCTION = "false"
11
+
12
+ [context.production.environment]
13
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
@@ -10,6 +10,9 @@ GTM_CONTAINER_ID = "GTM-N3WSTXZ"
10
10
  NITRO_PRESET = "netlify"
11
11
  NPM_CONFIG_PRODUCTION = "false"
12
12
 
13
+ [context.production.environment]
14
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
15
+
13
16
  # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
14
17
  # The framework does not impose a default agent-run timeout; if a template sets
15
18
  # AGENT_RUN_SOFT_TIMEOUT_MS, keep it below this function timeout so the hidden
@@ -9,3 +9,6 @@ GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
9
9
  GTM_CONTAINER_ID = "GTM-N3WSTXZ"
10
10
  NITRO_PRESET = "netlify"
11
11
  NPM_CONFIG_PRODUCTION = "false"
12
+
13
+ [context.production.environment]
14
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
@@ -0,0 +1,5 @@
1
+ import { defineAgentNativeConfig } from "@agent-native/core/config";
2
+
3
+ export default defineAgentNativeConfig({
4
+ harness: true,
5
+ });
@@ -10,6 +10,9 @@ GTM_CONTAINER_ID = "GTM-N3WSTXZ"
10
10
  NITRO_PRESET = "netlify"
11
11
  NPM_CONFIG_PRODUCTION = "false"
12
12
 
13
+ [context.production.environment]
14
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
15
+
13
16
  # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
14
17
  # The framework does not impose a default agent-run timeout; if a template sets
15
18
  # AGENT_RUN_SOFT_TIMEOUT_MS, keep it below this function timeout so the hidden
@@ -11,6 +11,9 @@ NITRO_PRESET = "netlify"
11
11
  NPM_CONFIG_PRODUCTION = "false"
12
12
  AGENT_CHAT_DURABLE_BACKGROUND = "true"
13
13
 
14
+ [context.production.environment]
15
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
16
+
14
17
  # A2A/MCP calls may include plan generation, artifact serialization, and
15
18
  # feedback polling. Keep this aligned with other agent-heavy hosted templates.
16
19
  # Agent chat runs are dispatched to the emitted Netlify background function;
@@ -14,6 +14,9 @@ NITRO_PRESET = "netlify"
14
14
  NPM_CONFIG_PRODUCTION = "false"
15
15
  AGENT_CHAT_DURABLE_BACKGROUND = "true"
16
16
 
17
+ [context.production.environment]
18
+ AGENT_NATIVE_HOSTED_HARNESS = "true"
19
+
17
20
  # Function wall for the non-agent long-request paths (deck import/export,
18
21
  # image generation). Agent chat opts into the separate Netlify background
19
22
  # function so hosted creative turns can use the longer background budget.
@@ -167,8 +167,12 @@ class AcpHarnessSession {
167
167
  this.acpSessionId = resume.sessionId;
168
168
  return;
169
169
  }
170
- catch {
171
- // Fall through to a fresh session if the agent could not load it.
170
+ catch (error) {
171
+ if (!isExplicitlyMissingAcpSession(error))
172
+ throw error;
173
+ // A provider can legitimately evict an old session. Only that
174
+ // explicit condition may fall back to a fresh session; transport or
175
+ // permission failures must remain visible to the caller.
172
176
  }
173
177
  }
174
178
  const created = await this.connection.newSession({
@@ -321,6 +325,9 @@ class AcpHarnessSession {
321
325
  return { content: sliceTextFile(raw, params.line, params.limit) };
322
326
  }
323
327
  async handleWriteTextFile(params) {
328
+ if (this.permissionMode === "allow-reads") {
329
+ throw new Error("[acp-harness] File writes are disabled in allow-reads mode.");
330
+ }
324
331
  const abs = resolveAcpWorkspacePath(this.cwd, params.path);
325
332
  const existed = await fileExists(abs);
326
333
  await fs.mkdir(path.dirname(abs), { recursive: true });
@@ -604,6 +611,14 @@ function acpErrorMessage(error) {
604
611
  }
605
612
  return typeof error === "string" ? error : "ACP request failed";
606
613
  }
614
+ function isExplicitlyMissingAcpSession(error) {
615
+ const message = error instanceof Error
616
+ ? error.message
617
+ : typeof error === "string"
618
+ ? error
619
+ : "";
620
+ return /(?:session|conversation).*(?:not found|does not exist|unknown|unsupported)|(?:unknown|unsupported).*(?:session|conversation)/i.test(message);
621
+ }
607
622
  /** Minimal async queue bridging ACP's callback updates to an async iterable. */
608
623
  class AsyncEventQueue {
609
624
  values = [];
@@ -199,8 +199,11 @@ async function createNativeSession(agent, options) {
199
199
  try {
200
200
  return await agent.createSession({ resumeState: options.resumeState });
201
201
  }
202
- catch {
203
- return agent.createSession();
202
+ catch (error) {
203
+ if (isExplicitlyMissingHarnessSession(error)) {
204
+ return agent.createSession();
205
+ }
206
+ throw error;
204
207
  }
205
208
  }
206
209
  if (typeof agent.createSession !== "function") {
@@ -237,7 +240,7 @@ class AiSdkHarnessSession {
237
240
  }
238
241
  async *continueTurn(input = {}) {
239
242
  if (typeof this.agent.continueStream !== "function") {
240
- return;
243
+ throw new Error("[agent-harness] This runtime cannot continue a harness turn.");
241
244
  }
242
245
  const result = await this.agent.continueStream({
243
246
  session: this.nativeSession,
@@ -353,3 +356,11 @@ function normalizeFileOperation(value) {
353
356
  ? value
354
357
  : "unknown";
355
358
  }
359
+ function isExplicitlyMissingHarnessSession(error) {
360
+ const message = error instanceof Error
361
+ ? error.message
362
+ : typeof error === "string"
363
+ ? error
364
+ : "";
365
+ return /(?:session|conversation).*(?:not found|does not exist|unknown|unsupported)|(?:unknown|unsupported).*(?:session|conversation)/i.test(message);
366
+ }
@@ -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 (status === "idle" || status === "stopped" || status === "destroyed") {
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 || session.ownerEmail !== scope.ownerEmail) {
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
- send({
10
- type: "activity",
11
- label: `Starting ${opts.adapter.label}`,
12
- tool: "harness",
13
- });
14
- harnessSession ??= await opts.adapter.createSession({
15
- ...(opts.createSession ?? {}),
16
- threadId: opts.threadId,
17
- runId: opts.runId,
18
- ownerEmail: opts.ownerEmail ?? opts.createSession?.ownerEmail ?? null,
19
- orgId: opts.orgId ?? opts.createSession?.orgId ?? null,
20
- signal,
21
- });
22
- await saveAgentHarnessSession({
23
- id: opts.createSession?.sessionId ?? harnessSession.id,
24
- harnessName: opts.adapter.name,
25
- threadId: opts.threadId,
26
- runId: opts.runId,
27
- providerSessionId: harnessSession.id,
28
- status: "running",
29
- resumeState: opts.createSession?.resumeState,
30
- ownerEmail: opts.ownerEmail ?? opts.createSession?.ownerEmail ?? null,
31
- orgId: opts.orgId ?? opts.createSession?.orgId ?? null,
32
- });
33
- const storedSessionId = opts.createSession?.sessionId ?? harnessSession.id;
34
- registerLiveAgentHarnessSession({
35
- sessionId: storedSessionId,
36
- adapter: opts.adapter,
37
- session: harnessSession,
38
- createSession: opts.createSession,
39
- ownerEmail: opts.ownerEmail ?? opts.createSession?.ownerEmail ?? null,
40
- orgId: opts.orgId ?? opts.createSession?.orgId ?? null,
41
- });
42
- const input = {
43
- ...opts.input,
44
- abortSignal: signal,
45
- };
46
- let pendingApproval = null;
47
- for await (const event of harnessSession.streamTurn(input)) {
48
- if (signal.aborted)
49
- break;
50
- await opts.onHarnessEvent?.(event);
51
- if (event.type === "approval-request") {
52
- pendingApproval = event;
53
- await updateAgentHarnessSession(opts.createSession?.sessionId ?? harnessSession.id, {
54
- status: "idle",
55
- pendingApproval: event,
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 (event.type === "error") {
59
- throw new Error(event.error);
68
+ if (signal.aborted) {
69
+ await stopHarnessSession(harnessSession);
70
+ releaseLiveAgentHarnessSession(storedSessionId, harnessSession);
71
+ await markAgentHarnessSessionStopped(storedSessionId, "stopped");
72
+ return;
60
73
  }
61
- for (const chatEvent of agentHarnessEventToAgentChatEvents(event)) {
62
- send(chatEvent);
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 markAgentHarnessSessionStopped(opts.createSession?.sessionId ?? harnessSession.id, "stopped");
69
- return;
94
+ await updateAgentHarnessSession(storedSessionId, {
95
+ status: "idle",
96
+ resumeState,
97
+ pendingApproval: null,
98
+ });
70
99
  }
71
- if (pendingApproval) {
72
- const stored = await getAgentHarnessSession(storedSessionId);
73
- const stillPending = stored?.pendingApproval &&
74
- typeof stored.pendingApproval === "object" &&
75
- "id" in stored.pendingApproval &&
76
- stored.pendingApproval.id === pendingApproval.id;
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: "idle",
80
- pendingApproval,
81
- });
82
- return;
107
+ status: "errored",
108
+ pendingApproval: null,
109
+ }).catch(() => undefined);
83
110
  }
111
+ throw error;
84
112
  }
85
- let resumeState = opts.createSession?.resumeState;
86
- if (detachOnComplete && harnessSession.detach) {
87
- resumeState = await harnessSession.detach();
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 */