@agent-native/core 0.157.9 → 0.157.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/templates/analytics/scripts/first-party-analytics-postgres-retention-dry-run.sql +5 -2
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +3 -1
- package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +18 -6
- package/corpus/templates/analytics/server/plugins/db.ts +139 -0
- package/corpus/templates/slides/AGENTS.md +14 -10
- package/corpus/templates/slides/actions/export-pptx.ts +1094 -94
- package/corpus/templates/slides/actions/import-file.ts +16 -1
- package/corpus/templates/slides/actions/import-pptx.ts +61 -43
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +222 -29
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +71 -0
- package/corpus/templates/slides/app/global.css +19 -1
- package/corpus/templates/slides/app/lib/export-google-slides-client.ts +44 -5
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +13 -6
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +556 -25
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +18 -1
- package/corpus/templates/slides/server/handlers/import/html-converter.ts +1176 -26
- package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +13 -0
- package/corpus/templates/slides/server/handlers/import/pptx-assets.ts +24 -0
- package/corpus/templates/slides/shared/aspect-ratios.ts +13 -1
- package/dist/agent/harness/migrations.js +24 -3
- package/dist/agent/production-agent.js +11 -0
- package/dist/agent/run-loop-with-resume.js +21 -4
- package/dist/automations/service.d.ts +6 -0
- package/dist/automations/service.js +35 -0
- package/dist/cli/code-agent-connector.d.ts +4 -1
- package/dist/cli/code-agent-connector.js +154 -23
- package/dist/cli/code-agent-executor.js +255 -13
- package/dist/cli/code-agent-runs.d.ts +1 -0
- package/dist/cli/code-agent-runs.js +2 -1
- package/dist/cli/portal-workspace.d.ts +49 -0
- package/dist/cli/portal-workspace.js +320 -0
- package/dist/client/AssistantChat.d.ts +1 -1
- package/dist/client/AssistantChat.js +39 -61
- package/dist/client/ConnectBuilderCard.js +33 -5
- package/dist/client/agent-chat-adapter.js +4 -0
- package/dist/client/analytics-platform.d.ts +2 -0
- package/dist/client/analytics-platform.js +12 -0
- package/dist/client/analytics.d.ts +3 -0
- package/dist/client/analytics.js +10 -1
- package/dist/client/chat/index.d.ts +1 -0
- package/dist/client/chat/index.js +1 -0
- package/dist/client/chat/run-recovery.d.ts +2 -1
- package/dist/client/chat/run-recovery.js +2 -2
- package/dist/client/chat-first/apps-rail.d.ts +1 -1
- package/dist/client/chat-first/apps-rail.js +11 -11
- package/dist/client/chat-first/surface-tabs.d.ts +1 -1
- package/dist/client/chat-first/surface-tabs.js +38 -36
- package/dist/client/chat-first/types.d.ts +5 -0
- package/dist/client/chat-first.d.ts +1 -1
- package/dist/client/chat-first.js +4 -4
- package/dist/client/desktop-local-code-change.d.ts +10 -0
- package/dist/client/desktop-local-code-change.js +16 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/observability/ThumbsFeedback.js +21 -7
- package/dist/client/terminal/AgentTerminal.d.ts +9 -1
- package/dist/client/terminal/AgentTerminal.js +38 -3
- package/dist/client/terminal/index.d.ts +1 -1
- package/dist/client/terminal/index.js +1 -1
- package/dist/client/track.js +9 -1
- package/dist/client/use-action.js +3 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +1 -1
- package/dist/ingestion/pptx.d.ts +78 -0
- package/dist/ingestion/pptx.js +991 -107
- package/dist/integrations/index.d.ts +3 -3
- package/dist/integrations/index.js +2 -2
- package/dist/integrations/plugin.js +95 -6
- package/dist/integrations/remote-commands-store.d.ts +5 -0
- package/dist/integrations/remote-commands-store.js +63 -22
- package/dist/integrations/remote-devices-store.d.ts +2 -1
- package/dist/integrations/remote-devices-store.js +46 -0
- package/dist/integrations/remote-types.d.ts +20 -0
- package/dist/jobs/actions/list-recurring-jobs.d.ts +3 -0
- package/dist/jobs/actions/list-recurring-jobs.js +5 -1
- package/dist/jobs/frontmatter.d.ts +16 -0
- package/dist/jobs/frontmatter.js +53 -0
- package/dist/jobs/remote-execution.d.ts +42 -0
- package/dist/jobs/remote-execution.js +287 -0
- package/dist/jobs/scheduler.js +102 -3
- package/dist/jobs/tools.js +50 -3
- package/dist/notifications/routes.d.ts +4 -4
- package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
- package/dist/provider-api/actions/provider-api.d.ts +14 -14
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/action-routes.js +4 -2
- package/dist/server/agent-chat-plugin.js +6 -0
- package/dist/server/agent-run-context.d.ts +1 -0
- package/dist/server/agent-run-context.js +9 -0
- package/dist/server/core-routes-plugin.js +8 -1
- package/dist/server/request-context.d.ts +2 -0
- package/dist/shared/analytics-platform.d.ts +6 -0
- package/dist/shared/analytics-platform.js +14 -0
- package/dist/shared/mcp-embed-headers.d.ts +1 -1
- package/dist/shared/mcp-embed-headers.js +1 -1
- package/dist/styles/agent-native.css +83 -103
- package/dist/terminal/cli-registry.js +5 -0
- package/dist/tracking/registry.js +9 -1
- package/dist/triggers/actions/list-automations.d.ts +3 -0
- package/dist/triggers/actions/list-automations.js +3 -0
- package/dist/triggers/actions/manage-automation.d.ts +9 -0
- package/dist/triggers/actions/manage-automation.js +14 -2
- package/dist/triggers/actions.js +82 -7
- package/docs/content/template-content-developers.mdx +48 -0
- package/docs/content/toolkit-ui.mdx +57 -0
- package/package.json +2 -2
|
@@ -621,6 +621,19 @@ export function detectBlockAlignment(
|
|
|
621
621
|
blockRight: number,
|
|
622
622
|
): "left" | "center" | "right" {
|
|
623
623
|
if (blockLines.length < 2) return "left";
|
|
624
|
+
// The block bounds come from these same lines. Equal-width lines therefore
|
|
625
|
+
// have identical left edges, right edges, and midpoints whether they were
|
|
626
|
+
// left-aligned or centered; keep that ambiguous case left-aligned rather
|
|
627
|
+
// than moving ordinary paragraphs to the center.
|
|
628
|
+
const widths = blockLines.map((line) => line.right - line.left);
|
|
629
|
+
const minWidth = Math.min(...widths);
|
|
630
|
+
const maxWidth = Math.max(...widths);
|
|
631
|
+
const sameBounds = blockLines.every(
|
|
632
|
+
(line) => line.left === blockLeft && line.right === blockRight,
|
|
633
|
+
);
|
|
634
|
+
if (maxWidth - minWidth <= TEXT_ALIGNMENT_TOLERANCE_PT && sameBounds) {
|
|
635
|
+
return "left";
|
|
636
|
+
}
|
|
624
637
|
const midpoint = (blockLeft + blockRight) / 2;
|
|
625
638
|
const centered = blockLines.every(
|
|
626
639
|
(line) =>
|
|
@@ -12,6 +12,30 @@ const BROWSER_RENDERABLE_IMAGE_MIME_TYPES = new Set([
|
|
|
12
12
|
"image/bmp",
|
|
13
13
|
]);
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Validate every image before any upload starts. PPTX imports reject partial
|
|
17
|
+
* fidelity, so an unsupported image must not leave earlier slide uploads
|
|
18
|
+
* orphaned when the action eventually throws.
|
|
19
|
+
*/
|
|
20
|
+
export function assertPptxImagesRenderable(
|
|
21
|
+
slides: readonly ParsedSlide[],
|
|
22
|
+
): void {
|
|
23
|
+
const unsupported = new Set<string>();
|
|
24
|
+
for (const slide of slides) {
|
|
25
|
+
for (const element of slide.elements ?? []) {
|
|
26
|
+
if (element.kind !== "image" || !element.image) continue;
|
|
27
|
+
if (!BROWSER_RENDERABLE_IMAGE_MIME_TYPES.has(element.image.mimeType)) {
|
|
28
|
+
unsupported.add(element.image.mimeType || "unknown");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (unsupported.size > 0) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Source-faithful PPTX import cannot preserve unsupported image type(s): ${[...unsupported].join(", ")}. No images were uploaded. Re-export the deck with browser-renderable images or use a PDF export for page-faithful preservation.`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
15
39
|
export async function uploadPptxSlideImages(args: {
|
|
16
40
|
slide: ParsedSlide;
|
|
17
41
|
slideIndex: number;
|
|
@@ -23,10 +23,22 @@ export const ASPECT_RATIOS = {
|
|
|
23
23
|
label: "4:5",
|
|
24
24
|
pptxInches: { w: 8, h: 10 },
|
|
25
25
|
},
|
|
26
|
+
"4:3": {
|
|
27
|
+
width: 960,
|
|
28
|
+
height: 720,
|
|
29
|
+
label: "4:3",
|
|
30
|
+
pptxInches: { w: 10, h: 7.5 },
|
|
31
|
+
},
|
|
26
32
|
} as const;
|
|
27
33
|
|
|
28
34
|
export type AspectRatio = keyof typeof ASPECT_RATIOS;
|
|
29
|
-
export const ASPECT_RATIO_VALUES = [
|
|
35
|
+
export const ASPECT_RATIO_VALUES = [
|
|
36
|
+
"16:9",
|
|
37
|
+
"1:1",
|
|
38
|
+
"9:16",
|
|
39
|
+
"4:5",
|
|
40
|
+
"4:3",
|
|
41
|
+
] as const;
|
|
30
42
|
export const DEFAULT_ASPECT_RATIO: AspectRatio = "16:9";
|
|
31
43
|
|
|
32
44
|
export function getAspectRatioDims(ratio: AspectRatio | undefined | null) {
|
|
@@ -10,8 +10,8 @@ export const AGENT_HARNESS_SESSION_MIGRATIONS = [
|
|
|
10
10
|
harness_name TEXT NOT NULL,
|
|
11
11
|
thread_id TEXT NOT NULL,
|
|
12
12
|
status TEXT NOT NULL DEFAULT 'idle',
|
|
13
|
-
created_at
|
|
14
|
-
updated_at
|
|
13
|
+
created_at BIGINT NOT NULL,
|
|
14
|
+
updated_at BIGINT NOT NULL
|
|
15
15
|
)
|
|
16
16
|
`,
|
|
17
17
|
},
|
|
@@ -27,7 +27,7 @@ export const AGENT_HARNESS_SESSION_MIGRATIONS = [
|
|
|
27
27
|
ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS resolved_approval_ids TEXT;
|
|
28
28
|
ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS owner_email TEXT;
|
|
29
29
|
ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS org_id TEXT;
|
|
30
|
-
ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS stopped_at
|
|
30
|
+
ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS stopped_at BIGINT;
|
|
31
31
|
CREATE INDEX IF NOT EXISTS idx_agent_harness_sessions_thread
|
|
32
32
|
ON agent_harness_sessions (thread_id, updated_at);
|
|
33
33
|
CREATE INDEX IF NOT EXISTS idx_agent_harness_sessions_status
|
|
@@ -36,4 +36,25 @@ export const AGENT_HARNESS_SESSION_MIGRATIONS = [
|
|
|
36
36
|
ON agent_harness_sessions (owner_email, updated_at)
|
|
37
37
|
`,
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
version: 3,
|
|
41
|
+
name: "agent-harness-sessions-widen-millisecond-timestamps",
|
|
42
|
+
// Existing release migrations may have created these columns as Postgres
|
|
43
|
+
// int4 before the timestamp contract was widened. The type change is
|
|
44
|
+
// additive and keeps Date.now() values representable on those databases.
|
|
45
|
+
sql: {
|
|
46
|
+
postgres: `
|
|
47
|
+
-- guard:allow-destructive-ddl — widen legacy int4 timestamp storage to preserve Date.now() values
|
|
48
|
+
ALTER TABLE agent_harness_sessions
|
|
49
|
+
ALTER COLUMN created_at TYPE BIGINT;
|
|
50
|
+
-- guard:allow-destructive-ddl — widen legacy int4 timestamp storage to preserve Date.now() values
|
|
51
|
+
ALTER TABLE agent_harness_sessions
|
|
52
|
+
ALTER COLUMN updated_at TYPE BIGINT;
|
|
53
|
+
-- guard:allow-destructive-ddl — widen legacy int4 timestamp storage to preserve Date.now() values
|
|
54
|
+
ALTER TABLE agent_harness_sessions
|
|
55
|
+
ALTER COLUMN stopped_at TYPE BIGINT
|
|
56
|
+
`,
|
|
57
|
+
sqlite: "",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
39
60
|
];
|
|
@@ -19,6 +19,7 @@ import { readBody } from "../server/h3-helpers.js";
|
|
|
19
19
|
import { resolveHostedHarnessPolicy } from "../server/hosted-harness-policy.js";
|
|
20
20
|
import { assertRequestActionSurfaceIsolation, getRequestRunContext, ensureRequestRunContext, getRequestContext, getRequestOrgId, getRequestUserEmail, runWithRequestContext, } from "../server/request-context.js";
|
|
21
21
|
import { fireInternalDispatch } from "../server/self-dispatch.js";
|
|
22
|
+
import { ANALYTICS_CLIENT_PLATFORM_BODY_FIELD } from "../shared/analytics-platform.js";
|
|
22
23
|
import { isReasoningEffort, normalizeReasoningEffortForRequest, stepDownReasoningEffort, } from "../shared/reasoning-effort.js";
|
|
23
24
|
import { actionPreparationContinuationNote } from "./action-continuation-guidance.js";
|
|
24
25
|
import { drainAgentWarnings, formatAgentWarningsForToolResult, } from "./action-warnings.js";
|
|
@@ -6226,6 +6227,16 @@ export function createProductionAgentHandler(options) {
|
|
|
6226
6227
|
isAgentChatDurableBackgroundEnabled({
|
|
6227
6228
|
appOptIn: options.durableBackgroundRuns,
|
|
6228
6229
|
});
|
|
6230
|
+
const mutableBody = body;
|
|
6231
|
+
if (!isBackgroundWorker) {
|
|
6232
|
+
delete mutableBody[ANALYTICS_CLIENT_PLATFORM_BODY_FIELD];
|
|
6233
|
+
}
|
|
6234
|
+
if (dispatchToBackground) {
|
|
6235
|
+
const clientPlatform = getRequestContext()?.clientPlatform;
|
|
6236
|
+
if (clientPlatform) {
|
|
6237
|
+
mutableBody[ANALYTICS_CLIENT_PLATFORM_BODY_FIELD] = clientPlatform;
|
|
6238
|
+
}
|
|
6239
|
+
}
|
|
6229
6240
|
const requestBrowserTabId = normalizeBrowserTabId(browserTabId);
|
|
6230
6241
|
const requestChatScope = normalizeChatScope(scope);
|
|
6231
6242
|
const requestRunCtx = ensureRequestRunContext();
|
|
@@ -407,7 +407,11 @@ export async function runAgentLoopDirectWithSoftTimeout(opts, softTimeoutMs, tim
|
|
|
407
407
|
const internalContinuationReason = internalContinuationReasonForAttempt(attemptEvents);
|
|
408
408
|
if (internalContinuationReason && !upstreamSignal.aborted) {
|
|
409
409
|
lastAttemptWasUnfinishedContinuation = true;
|
|
410
|
-
|
|
410
|
+
const continuationEvents = [...localTurnEvents];
|
|
411
|
+
if ((await completedSideEffectInCurrentTurn(opts.threadId, opts.turnId, continuationEvents)) === "none") {
|
|
412
|
+
opts.send({ type: "clear" });
|
|
413
|
+
}
|
|
414
|
+
await appendContinuationAndJournal(opts.messages, internalContinuationReason, opts.threadId, opts.turnId, continuationEvents, attemptEvents);
|
|
411
415
|
continue;
|
|
412
416
|
}
|
|
413
417
|
if (softTimedOut && !upstreamSignal.aborted) {
|
|
@@ -422,7 +426,12 @@ export async function runAgentLoopDirectWithSoftTimeout(opts, softTimeoutMs, tim
|
|
|
422
426
|
}
|
|
423
427
|
catch (err) {
|
|
424
428
|
if (softTimedOut && !upstreamSignal.aborted) {
|
|
429
|
+
// Clear partial text the client received before the abort so the
|
|
430
|
+
// resumed model doesn't re-emit it and produce duplicated output.
|
|
425
431
|
lastAttemptWasUnfinishedContinuation = true;
|
|
432
|
+
if ((await completedSideEffectInCurrentTurn(opts.threadId, opts.turnId, localTurnEvents)) === "none") {
|
|
433
|
+
opts.send({ type: "clear" });
|
|
434
|
+
}
|
|
426
435
|
await appendContinuationAndJournal(opts.messages, "run_timeout", opts.threadId, opts.turnId, localTurnEvents, localTurnEvents.slice(attemptStartIndex));
|
|
427
436
|
continue;
|
|
428
437
|
}
|
|
@@ -439,6 +448,9 @@ export async function runAgentLoopDirectWithSoftTimeout(opts, softTimeoutMs, tim
|
|
|
439
448
|
rateLimitRetryFitsBudget) {
|
|
440
449
|
lastAttemptWasUnfinishedContinuation = true;
|
|
441
450
|
backgroundRateLimitContinuations++;
|
|
451
|
+
if ((await completedSideEffectInCurrentTurn(opts.threadId, opts.turnId, localTurnEvents)) === "none") {
|
|
452
|
+
opts.send({ type: "clear" });
|
|
453
|
+
}
|
|
442
454
|
await appendContinuationAndJournal(opts.messages, "rate_limited", opts.threadId, opts.turnId, localTurnEvents, localTurnEvents.slice(attemptStartIndex));
|
|
443
455
|
await waitForBackgroundRateLimitCooldown(upstreamSignal);
|
|
444
456
|
continue;
|
|
@@ -451,11 +463,16 @@ export async function runAgentLoopDirectWithSoftTimeout(opts, softTimeoutMs, tim
|
|
|
451
463
|
// another LLM call. Anthropic's prompt cache makes the resume call much
|
|
452
464
|
// faster than the cold first attempt.
|
|
453
465
|
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
466
|
+
// Emit 'clear' so any partial streamed text is discarded on the client
|
|
467
|
+
// before the model resumes. Without this the model restarts its sentence
|
|
468
|
+
// from scratch and the fold produces duplicated text in one message
|
|
469
|
+
// (the partial text was already sent to the client but is now retained
|
|
470
|
+
// only as an internal checkpoint so the next attempt can finish it).
|
|
457
471
|
if (!upstreamSignal.aborted && isResumableEngineError(err)) {
|
|
458
472
|
lastAttemptWasUnfinishedContinuation = true;
|
|
473
|
+
if ((await completedSideEffectInCurrentTurn(opts.threadId, opts.turnId, localTurnEvents)) === "none") {
|
|
474
|
+
opts.send({ type: "clear" });
|
|
475
|
+
}
|
|
459
476
|
await appendContinuationAndJournal(opts.messages, continuationReasonForResumableError(err), opts.threadId, opts.turnId, localTurnEvents, localTurnEvents.slice(attemptStartIndex));
|
|
460
477
|
continue;
|
|
461
478
|
}
|
|
@@ -38,6 +38,9 @@ export interface DefineAutomationInput {
|
|
|
38
38
|
domain?: string;
|
|
39
39
|
delegatedPolicyId?: string;
|
|
40
40
|
model?: string;
|
|
41
|
+
executionHostId?: string;
|
|
42
|
+
executionEngine?: string;
|
|
43
|
+
executionCwd?: string;
|
|
41
44
|
mcpTools?: unknown;
|
|
42
45
|
delivery?: AutomationDelivery;
|
|
43
46
|
}
|
|
@@ -52,6 +55,9 @@ export interface UpdateAutomationInput {
|
|
|
52
55
|
schedule?: string;
|
|
53
56
|
timezone?: string;
|
|
54
57
|
model?: string | null;
|
|
58
|
+
executionHostId?: string | null;
|
|
59
|
+
executionEngine?: string | null;
|
|
60
|
+
executionCwd?: string | null;
|
|
55
61
|
mcpTools?: unknown;
|
|
56
62
|
}
|
|
57
63
|
/**
|
|
@@ -9,6 +9,20 @@ export const DEFAULT_AUTOMATION_SCHEDULE = "0 * * * *";
|
|
|
9
9
|
function httpError(message, statusCode) {
|
|
10
10
|
return Object.assign(new Error(message), { statusCode });
|
|
11
11
|
}
|
|
12
|
+
function normalizeExecutionTarget(value, label, options = {}) {
|
|
13
|
+
if (value === null || value === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
const normalized = value.trim();
|
|
16
|
+
if (!normalized)
|
|
17
|
+
return undefined;
|
|
18
|
+
const max = options.max ?? (options.opaque ? 128 : 1024);
|
|
19
|
+
if (normalized.length > max ||
|
|
20
|
+
/[\r\n]/.test(normalized) ||
|
|
21
|
+
(options.opaque && !/^[a-z0-9][a-z0-9._:-]{0,127}$/i.test(normalized))) {
|
|
22
|
+
throw httpError(`${label} must be a bounded identifier${options.opaque ? " using letters, numbers, dots, underscores, colons, or hyphens" : ""}.`, 400);
|
|
23
|
+
}
|
|
24
|
+
return normalized;
|
|
25
|
+
}
|
|
12
26
|
function normalizeActor(actor) {
|
|
13
27
|
const userEmail = actor.userEmail.trim().toLowerCase();
|
|
14
28
|
if (!userEmail)
|
|
@@ -204,6 +218,12 @@ export async function defineAutomation(actorInput, input) {
|
|
|
204
218
|
? input.timezone || (await resolveUserSchedulingTimezone(actor.userEmail))
|
|
205
219
|
: undefined;
|
|
206
220
|
const mcpTools = normalizeJobMcpTools(input.mcpTools);
|
|
221
|
+
const executionHostId = normalizeExecutionTarget(input.executionHostId, "execution_host_id", { opaque: true });
|
|
222
|
+
const executionEngine = normalizeExecutionTarget(input.executionEngine, "execution_engine", { opaque: true });
|
|
223
|
+
const executionCwd = normalizeExecutionTarget(input.executionCwd, "execution_cwd");
|
|
224
|
+
if (executionHostId && input.triggerType !== "schedule") {
|
|
225
|
+
throw httpError("Execution hosts are currently supported for scheduled code automations only.", 400);
|
|
226
|
+
}
|
|
207
227
|
const meta = {
|
|
208
228
|
schedule: input.triggerType === "schedule" ? schedule : "",
|
|
209
229
|
timezone,
|
|
@@ -222,6 +242,9 @@ export async function defineAutomation(actorInput, input) {
|
|
|
222
242
|
? nextOccurrence(schedule, undefined, timezone).toISOString()
|
|
223
243
|
: undefined,
|
|
224
244
|
model: input.model?.trim() || undefined,
|
|
245
|
+
executionHostId,
|
|
246
|
+
executionEngine,
|
|
247
|
+
executionCwd,
|
|
225
248
|
mcpTools: mcpTools?.length ? mcpTools : undefined,
|
|
226
249
|
originScopeId: input.delivery?.originScopeId,
|
|
227
250
|
deliveryPlatform: input.delivery?.platform,
|
|
@@ -283,6 +306,18 @@ export async function updateAutomation(actorInput, input) {
|
|
|
283
306
|
if (input.model !== undefined) {
|
|
284
307
|
meta.model = input.model?.trim() || undefined;
|
|
285
308
|
}
|
|
309
|
+
if (input.executionHostId !== undefined) {
|
|
310
|
+
if (input.executionHostId && meta.triggerType !== "schedule") {
|
|
311
|
+
throw httpError("Execution hosts are currently supported for scheduled code automations only.", 400);
|
|
312
|
+
}
|
|
313
|
+
meta.executionHostId = normalizeExecutionTarget(input.executionHostId, "execution_host_id", { opaque: true });
|
|
314
|
+
}
|
|
315
|
+
if (input.executionEngine !== undefined) {
|
|
316
|
+
meta.executionEngine = normalizeExecutionTarget(input.executionEngine, "execution_engine", { opaque: true });
|
|
317
|
+
}
|
|
318
|
+
if (input.executionCwd !== undefined) {
|
|
319
|
+
meta.executionCwd = normalizeExecutionTarget(input.executionCwd, "execution_cwd");
|
|
320
|
+
}
|
|
286
321
|
if (input.mcpTools !== undefined) {
|
|
287
322
|
const mcpTools = normalizeJobMcpTools(input.mcpTools);
|
|
288
323
|
meta.mcpTools = mcpTools?.length ? mcpTools : undefined;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { RemoteComputerCapabilities } from "../integrations/remote-types.js";
|
|
1
|
+
import type { RemoteComputerCapabilities, RemoteExecutionCapabilities } from "../integrations/remote-types.js";
|
|
2
2
|
export interface RemoteCodeAgentDeviceConfig {
|
|
3
3
|
token: string;
|
|
4
4
|
relayUrl?: string;
|
|
5
5
|
deviceId?: string;
|
|
6
6
|
deviceName?: string;
|
|
7
|
+
/** Default host-local workspace used when a command omits cwd. */
|
|
8
|
+
workspacePath?: string;
|
|
7
9
|
pollIntervalMs?: number;
|
|
8
10
|
}
|
|
9
11
|
export interface RunCodeAgentConnectorOptions {
|
|
@@ -17,6 +19,7 @@ export interface LocalComputerBridgeConfig {
|
|
|
17
19
|
token: string;
|
|
18
20
|
capabilities: RemoteComputerCapabilities;
|
|
19
21
|
}
|
|
22
|
+
export declare const DEFAULT_REMOTE_EXECUTION_CAPABILITIES: RemoteExecutionCapabilities;
|
|
20
23
|
export declare function remoteDeviceConfigPath(): string;
|
|
21
24
|
export declare function loadRemoteCodeAgentDeviceConfig(configPath?: string): RemoteCodeAgentDeviceConfig | null;
|
|
22
25
|
export declare function runCodeAgentConnector(options?: RunCodeAgentConnectorOptions): Promise<number>;
|
|
@@ -6,14 +6,25 @@ import { assertValidComputerCommandEnvelope } from "../integrations/computer-sup
|
|
|
6
6
|
import { serializeBoundedRemoteJson } from "../integrations/remote-json-safety.js";
|
|
7
7
|
import { executeDenyCodeAgentApproval, executePendingCodeAgentApproval, } from "./code-agent-executor.js";
|
|
8
8
|
import { appendCodeAgentTranscriptEvent, codeAgentRunTranscriptPath, codeAgentStoreRoot, createCodeAgentRunRecord, getCodeAgentRunRecord, listCodeAgentRunRecords, normalizeCodeAgentPermissionMode, queueCodeAgentFollowUp, updateCodeAgentRunRecord, } from "./code-agent-runs.js";
|
|
9
|
+
import { loadPortalEnvironment, preparePortalWorkspace, parsePortalHandoff, } from "./portal-workspace.js";
|
|
9
10
|
const DEVICE_PATH_ENV = "AGENT_NATIVE_REMOTE_DEVICE_PATH";
|
|
10
11
|
const COMPUTER_BRIDGE_URL_ENV = "AGENT_NATIVE_COMPUTER_BRIDGE_URL";
|
|
11
12
|
const COMPUTER_BRIDGE_TOKEN_ENV = "AGENT_NATIVE_COMPUTER_BRIDGE_TOKEN";
|
|
12
13
|
const COMPUTER_CAPABILITIES_ENV = "AGENT_NATIVE_COMPUTER_CAPABILITIES";
|
|
14
|
+
const REMOTE_ENGINES_ENV = "AGENT_NATIVE_REMOTE_ENGINES";
|
|
13
15
|
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
14
16
|
const MAX_POLL_INTERVAL_MS = 30_000;
|
|
15
17
|
const MAX_TRANSCRIPT_EVENTS_PER_BATCH = 50;
|
|
16
18
|
const activeRunners = new Map();
|
|
19
|
+
export const DEFAULT_REMOTE_EXECUTION_CAPABILITIES = {
|
|
20
|
+
backend: "desktop",
|
|
21
|
+
workloads: ["code-agent", "scheduled-code"],
|
|
22
|
+
engines: ["codex-cli", "claude-cli", "pi-cli", "opencode-cli"],
|
|
23
|
+
acceptsScheduledWork: true,
|
|
24
|
+
acceptsPortalHandoffs: true,
|
|
25
|
+
persistence: "local-files",
|
|
26
|
+
adapters: ["agent-native-code"],
|
|
27
|
+
};
|
|
17
28
|
export function remoteDeviceConfigPath() {
|
|
18
29
|
return path.resolve(process.env[DEVICE_PATH_ENV] ??
|
|
19
30
|
path.join(os.homedir(), ".agent-native", "remote-device.json"));
|
|
@@ -32,6 +43,7 @@ export function loadRemoteCodeAgentDeviceConfig(configPath = remoteDeviceConfigP
|
|
|
32
43
|
relayUrl: firstStringValue(raw.relayUrl, raw.url, raw.baseUrl),
|
|
33
44
|
deviceId: firstStringValue(raw.deviceId, raw.id),
|
|
34
45
|
deviceName: firstStringValue(raw.deviceName, raw.name),
|
|
46
|
+
workspacePath: firstStringValue(raw.workspacePath, raw.workspace, raw.cwd, raw.projectPath),
|
|
35
47
|
pollIntervalMs: Number.isFinite(pollIntervalMs) && pollIntervalMs > 0
|
|
36
48
|
? Math.min(Math.max(pollIntervalMs, 500), MAX_POLL_INTERVAL_MS)
|
|
37
49
|
: undefined,
|
|
@@ -77,6 +89,7 @@ class RemoteCodeAgentConnector {
|
|
|
77
89
|
output;
|
|
78
90
|
transcriptCursors = new Map();
|
|
79
91
|
remoteRunIds = new Set();
|
|
92
|
+
terminalCommandIds = new Set();
|
|
80
93
|
stopped = false;
|
|
81
94
|
computerBridge = loadLocalComputerBridgeConfig();
|
|
82
95
|
constructor(config, relayUrl, output) {
|
|
@@ -140,6 +153,7 @@ class RemoteCodeAgentConnector {
|
|
|
140
153
|
"run-events",
|
|
141
154
|
],
|
|
142
155
|
computerCapabilities: this.computerBridge?.capabilities ?? {},
|
|
156
|
+
executionCapabilities: remoteExecutionCapabilities(),
|
|
143
157
|
activeRunIds: Array.from(this.remoteRunIds),
|
|
144
158
|
});
|
|
145
159
|
const commands = normalizeCommands(response);
|
|
@@ -180,25 +194,65 @@ class RemoteCodeAgentConnector {
|
|
|
180
194
|
async computerOperation(command) {
|
|
181
195
|
return dispatchComputerOperationToLocalBridge(this.computerBridge, command.params.envelope);
|
|
182
196
|
}
|
|
183
|
-
createRun(command) {
|
|
197
|
+
async createRun(command) {
|
|
184
198
|
const prompt = firstTextValue(command.params.prompt, command.params.message, command.params.input);
|
|
185
199
|
if (!prompt) {
|
|
186
200
|
return { ok: false, error: "Missing prompt." };
|
|
187
201
|
}
|
|
188
202
|
const goalId = firstStringValue(command.params.goalId) ?? "task";
|
|
189
|
-
const
|
|
203
|
+
const metadata = isObject(command.params.metadata)
|
|
204
|
+
? command.params.metadata
|
|
205
|
+
: {};
|
|
206
|
+
let cwd = resolveCommandCwd(command.params.cwd ?? this.config.workspacePath);
|
|
207
|
+
let portalWorkspace;
|
|
208
|
+
let portalEnvironment;
|
|
209
|
+
if (metadata.portal !== undefined) {
|
|
210
|
+
try {
|
|
211
|
+
portalWorkspace = await preparePortalWorkspace({
|
|
212
|
+
handoff: parsePortalHandoff(metadata.portal),
|
|
213
|
+
workspacePath: this.config.workspacePath,
|
|
214
|
+
});
|
|
215
|
+
portalEnvironment = loadPortalEnvironment(portalWorkspace.environmentRoot);
|
|
216
|
+
cwd = portalWorkspace.workspacePath;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
error: error instanceof Error ? error.message : String(error),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const requestedRunId = firstStringValue(command.params.runId);
|
|
226
|
+
if (portalWorkspace && requestedRunId && !isSafeRunId(requestedRunId)) {
|
|
227
|
+
return { ok: false, error: "Portal run id is invalid." };
|
|
228
|
+
}
|
|
229
|
+
if (portalWorkspace && requestedRunId) {
|
|
230
|
+
const existing = getCodeAgentRunRecord(requestedRunId);
|
|
231
|
+
const existingRemote = isObject(existing?.metadata?.remote)
|
|
232
|
+
? existing.metadata.remote
|
|
233
|
+
: {};
|
|
234
|
+
if (existing &&
|
|
235
|
+
firstStringValue(existingRemote.commandId) === command.id) {
|
|
236
|
+
this.remoteRunIds.add(existing.id);
|
|
237
|
+
this.transcriptCursors.set(existing.id, { offset: 0, seq: 0 });
|
|
238
|
+
if (!activeRunners.has(existing.id)) {
|
|
239
|
+
this.spawnRunner(existing.id, existing.cwd, existing.permissionMode, portalEnvironment?.values);
|
|
240
|
+
}
|
|
241
|
+
return { ok: true, runId: existing.id, run: existing, resumed: true };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
190
244
|
const permissionMode = normalizeCodeAgentPermissionMode(command.params.permissionMode) ??
|
|
191
245
|
"full-auto";
|
|
192
246
|
const engine = firstStringValue(command.params.engine);
|
|
193
247
|
const model = firstStringValue(command.params.model);
|
|
194
248
|
const effort = firstStringValue(command.params.effort, command.params.reasoningEffort);
|
|
195
|
-
const metadata = isObject(command.params.metadata)
|
|
196
|
-
? command.params.metadata
|
|
197
|
-
: {};
|
|
198
249
|
const run = createCodeAgentRunRecord({
|
|
250
|
+
...(portalWorkspace && requestedRunId ? { id: requestedRunId } : {}),
|
|
199
251
|
goalId,
|
|
200
252
|
title: firstStringValue(command.params.title) ?? titleFromPrompt(prompt),
|
|
201
|
-
subtitle:
|
|
253
|
+
subtitle: portalWorkspace
|
|
254
|
+
? `Portal on ${this.config.deviceName ?? "paired computer"}`
|
|
255
|
+
: "Remote coding task",
|
|
202
256
|
status: "queued",
|
|
203
257
|
phase: "queued",
|
|
204
258
|
permissionMode,
|
|
@@ -213,6 +267,20 @@ class RemoteCodeAgentConnector {
|
|
|
213
267
|
{ label: "Prompt", value: truncateForDisplay(prompt, 160) },
|
|
214
268
|
{ label: "Agent", value: "Remote connector" },
|
|
215
269
|
{ label: "Mode", value: permissionMode },
|
|
270
|
+
...(portalWorkspace
|
|
271
|
+
? [
|
|
272
|
+
{
|
|
273
|
+
label: "Workspace",
|
|
274
|
+
value: `Portal · ${portalWorkspace.workspacePath}`,
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
label: "Environment",
|
|
278
|
+
value: portalEnvironment?.files.length
|
|
279
|
+
? `Loaded ${portalEnvironment.files.join(", ")}`
|
|
280
|
+
: "Using paired computer environment",
|
|
281
|
+
},
|
|
282
|
+
]
|
|
283
|
+
: []),
|
|
216
284
|
],
|
|
217
285
|
metadata: {
|
|
218
286
|
...metadata,
|
|
@@ -225,7 +293,26 @@ class RemoteCodeAgentConnector {
|
|
|
225
293
|
commandId: command.id,
|
|
226
294
|
deviceId: this.config.deviceId,
|
|
227
295
|
relayUrl: this.relayUrl,
|
|
296
|
+
...(portalWorkspace ? { remoteRunId: run.id } : {}),
|
|
228
297
|
},
|
|
298
|
+
...(portalWorkspace
|
|
299
|
+
? {
|
|
300
|
+
portal: portalWorkspace.handoff,
|
|
301
|
+
executionResidence: {
|
|
302
|
+
schemaVersion: 1,
|
|
303
|
+
kind: "portal",
|
|
304
|
+
hostId: this.config.deviceId,
|
|
305
|
+
hostLabel: this.config.deviceName ?? os.hostname(),
|
|
306
|
+
workspacePath: portalWorkspace.workspacePath,
|
|
307
|
+
sourceBranch: portalWorkspace.handoff.sourceBranch,
|
|
308
|
+
sourceCommit: portalWorkspace.handoff.commit,
|
|
309
|
+
handoffId: portalWorkspace.handoff.handoffId,
|
|
310
|
+
envPolicy: portalWorkspace.handoff.envPolicy,
|
|
311
|
+
environmentFiles: portalEnvironment?.files ?? [],
|
|
312
|
+
environmentLoadedAt: new Date().toISOString(),
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
: {}),
|
|
229
316
|
},
|
|
230
317
|
});
|
|
231
318
|
appendCodeAgentTranscriptEvent({
|
|
@@ -242,7 +329,7 @@ class RemoteCodeAgentConnector {
|
|
|
242
329
|
});
|
|
243
330
|
this.remoteRunIds.add(run.id);
|
|
244
331
|
this.transcriptCursors.set(run.id, { offset: 0, seq: 0 });
|
|
245
|
-
this.spawnRunner(run.id, cwd, permissionMode);
|
|
332
|
+
this.spawnRunner(run.id, cwd, permissionMode, portalEnvironment?.values);
|
|
246
333
|
return { ok: true, runId: run.id, run };
|
|
247
334
|
}
|
|
248
335
|
appendFollowUp(command) {
|
|
@@ -424,7 +511,7 @@ class RemoteCodeAgentConnector {
|
|
|
424
511
|
activeRunIds: Array.from(activeRunners.keys()),
|
|
425
512
|
};
|
|
426
513
|
}
|
|
427
|
-
spawnRunner(runId, cwd, permissionMode) {
|
|
514
|
+
spawnRunner(runId, cwd, permissionMode, portalEnvironment) {
|
|
428
515
|
if (activeRunners.has(runId))
|
|
429
516
|
return;
|
|
430
517
|
const invocation = resolveCodeAgentCliInvocation();
|
|
@@ -434,6 +521,7 @@ class RemoteCodeAgentConnector {
|
|
|
434
521
|
stdio: ["ignore", "pipe", "pipe"],
|
|
435
522
|
env: {
|
|
436
523
|
...process.env,
|
|
524
|
+
...portalEnvironment,
|
|
437
525
|
AGENT_NATIVE_CODE_AGENTS_HOME: codeAgentStoreRoot(),
|
|
438
526
|
...(permissionMode
|
|
439
527
|
? { AGENT_NATIVE_CODE_AGENT_PERMISSION_MODE: permissionMode }
|
|
@@ -484,28 +572,50 @@ class RemoteCodeAgentConnector {
|
|
|
484
572
|
for (const runId of this.remoteRunIds) {
|
|
485
573
|
const cursor = this.transcriptCursors.get(runId) ?? { offset: 0, seq: 0 };
|
|
486
574
|
const batch = readTranscriptBatch(runId, cursor.offset);
|
|
487
|
-
if (batch.events.length
|
|
488
|
-
this.
|
|
489
|
-
|
|
490
|
-
|
|
575
|
+
if (batch.events.length > 0) {
|
|
576
|
+
await this.postJson("/_agent-native/integrations/remote/run-events", {
|
|
577
|
+
deviceId: this.config.deviceId,
|
|
578
|
+
remoteRunId: runId,
|
|
579
|
+
cursor: { offset: batch.nextOffset },
|
|
580
|
+
events: batch.events.map((event, index) => ({
|
|
581
|
+
seq: cursor.seq + index,
|
|
582
|
+
event,
|
|
583
|
+
})),
|
|
491
584
|
});
|
|
492
|
-
continue;
|
|
493
585
|
}
|
|
494
|
-
await this.postJson("/_agent-native/integrations/remote/run-events", {
|
|
495
|
-
deviceId: this.config.deviceId,
|
|
496
|
-
remoteRunId: runId,
|
|
497
|
-
cursor: { offset: batch.nextOffset },
|
|
498
|
-
events: batch.events.map((event, index) => ({
|
|
499
|
-
seq: cursor.seq + index,
|
|
500
|
-
event,
|
|
501
|
-
})),
|
|
502
|
-
});
|
|
503
586
|
this.transcriptCursors.set(runId, {
|
|
504
587
|
offset: batch.nextOffset,
|
|
505
588
|
seq: cursor.seq + batch.events.length,
|
|
506
589
|
});
|
|
590
|
+
await this.reportRemoteRunTerminal(runId);
|
|
507
591
|
}
|
|
508
592
|
}
|
|
593
|
+
async reportRemoteRunTerminal(runId) {
|
|
594
|
+
const run = getCodeAgentRunRecord(runId);
|
|
595
|
+
if (!run || (run.status !== "completed" && run.status !== "errored")) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const remote = isObject(run.metadata?.remote) ? run.metadata.remote : {};
|
|
599
|
+
const commandId = firstStringValue(remote.commandId);
|
|
600
|
+
if (!commandId || this.terminalCommandIds.has(commandId))
|
|
601
|
+
return;
|
|
602
|
+
const errorMessage = firstStringValue(run.metadata?.error, run.metadata?.runnerError);
|
|
603
|
+
await this.postJson("/_agent-native/integrations/remote/result", {
|
|
604
|
+
deviceId: this.config.deviceId,
|
|
605
|
+
commandId,
|
|
606
|
+
kind: "create-run",
|
|
607
|
+
ok: run.status === "completed",
|
|
608
|
+
status: run.status === "completed" ? "completed" : "failed",
|
|
609
|
+
result: {
|
|
610
|
+
ok: run.status === "completed",
|
|
611
|
+
run,
|
|
612
|
+
},
|
|
613
|
+
errorMessage: run.status === "errored"
|
|
614
|
+
? (errorMessage ?? "Remote code-agent run failed.")
|
|
615
|
+
: undefined,
|
|
616
|
+
});
|
|
617
|
+
this.terminalCommandIds.add(commandId);
|
|
618
|
+
}
|
|
509
619
|
async postCommandResult(command, result) {
|
|
510
620
|
let safeResult;
|
|
511
621
|
try {
|
|
@@ -519,12 +629,17 @@ class RemoteCodeAgentConnector {
|
|
|
519
629
|
: "Connector result rejected as unsafe.",
|
|
520
630
|
};
|
|
521
631
|
}
|
|
632
|
+
const isAcceptedCreateRun = normalizeKind(command.kind) === "create-run" && safeResult.ok !== false;
|
|
522
633
|
await this.postJson("/_agent-native/integrations/remote/result", {
|
|
523
634
|
deviceId: this.config.deviceId,
|
|
524
635
|
commandId: command.id,
|
|
525
636
|
kind: command.kind,
|
|
526
637
|
ok: safeResult.ok !== false,
|
|
527
|
-
status: safeResult.ok === false
|
|
638
|
+
status: safeResult.ok === false
|
|
639
|
+
? "failed"
|
|
640
|
+
: isAcceptedCreateRun
|
|
641
|
+
? "running"
|
|
642
|
+
: "completed",
|
|
528
643
|
result: safeResult,
|
|
529
644
|
errorMessage: typeof safeResult.error === "string" ? safeResult.error : undefined,
|
|
530
645
|
});
|
|
@@ -535,6 +650,7 @@ class RemoteCodeAgentConnector {
|
|
|
535
650
|
method: "POST",
|
|
536
651
|
headers: {
|
|
537
652
|
authorization: `Bearer ${this.config.token}`,
|
|
653
|
+
"x-agent-native-device-token": this.config.token,
|
|
538
654
|
"content-type": "application/json",
|
|
539
655
|
},
|
|
540
656
|
body: JSON.stringify(body),
|
|
@@ -712,6 +828,21 @@ function resolveCommandCwd(value) {
|
|
|
712
828
|
const cwd = firstStringValue(value);
|
|
713
829
|
return cwd ? path.resolve(cwd) : process.cwd();
|
|
714
830
|
}
|
|
831
|
+
function isSafeRunId(value) {
|
|
832
|
+
return value.length <= 160 && /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value);
|
|
833
|
+
}
|
|
834
|
+
function remoteExecutionCapabilities() {
|
|
835
|
+
const configured = process.env[REMOTE_ENGINES_ENV]
|
|
836
|
+
?.split(",")
|
|
837
|
+
.map((engine) => engine.trim())
|
|
838
|
+
.filter(Boolean);
|
|
839
|
+
return {
|
|
840
|
+
...DEFAULT_REMOTE_EXECUTION_CAPABILITIES,
|
|
841
|
+
...(configured?.length
|
|
842
|
+
? { engines: [...new Set(configured)].slice(0, 32) }
|
|
843
|
+
: {}),
|
|
844
|
+
};
|
|
845
|
+
}
|
|
715
846
|
function normalizeRelayUrl(value) {
|
|
716
847
|
if (!value)
|
|
717
848
|
return null;
|