experimental-ash 0.25.1 → 0.25.2

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 (58) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/src/channel/routes.d.ts +6 -1
  3. package/dist/src/channel/send.js +5 -2
  4. package/dist/src/channel/session-callback.d.ts +10 -0
  5. package/dist/src/channel/session-callback.js +65 -0
  6. package/dist/src/channel/types.d.ts +19 -0
  7. package/dist/src/chunks/{client-BShLWzR6.js → client-ZqNLLMZB.js} +3 -3
  8. package/dist/src/chunks/{compile-agent-CyP6FrL8.js → compile-agent-C4OrJW6C.js} +1 -1
  9. package/dist/src/chunks/{dev-authored-source-watcher-DIWfVUsu.js → dev-authored-source-watcher-PwAxalwl.js} +1 -1
  10. package/dist/src/chunks/host-F-DkwYJK.js +70 -0
  11. package/dist/src/chunks/paths-DnlVBqHu.js +85 -0
  12. package/dist/src/chunks/{token-BOkIxJeV.js → token-YW4VSeBB.js} +1 -1
  13. package/dist/src/chunks/types-BJSR0JNV.js +1 -0
  14. package/dist/src/cli/commands/info.js +1 -1
  15. package/dist/src/cli/dev/repl.js +3 -3
  16. package/dist/src/cli/run.js +1 -1
  17. package/dist/src/client/message-reducer.js +6 -0
  18. package/dist/src/context/keys.d.ts +1 -1
  19. package/dist/src/context/keys.js +1 -1
  20. package/dist/src/context/seed-keys.d.ts +5 -1
  21. package/dist/src/context/seed-keys.js +4 -0
  22. package/dist/src/evals/cli/eval.js +1 -1
  23. package/dist/src/execution/await-authorization-orchestrator.js +1 -1
  24. package/dist/src/execution/node-step.js +13 -0
  25. package/dist/src/execution/remote-agent-dispatch.d.ts +15 -0
  26. package/dist/src/execution/remote-agent-dispatch.js +79 -0
  27. package/dist/src/execution/runtime-context.js +4 -1
  28. package/dist/src/execution/session-callback-step.d.ts +16 -0
  29. package/dist/src/execution/session-callback-step.js +72 -0
  30. package/dist/src/execution/subagent-invocation.d.ts +16 -0
  31. package/dist/src/execution/subagent-invocation.js +16 -0
  32. package/dist/src/execution/subagent-tool.js +5 -8
  33. package/dist/src/execution/workflow-entry.js +21 -1
  34. package/dist/src/execution/workflow-steps.d.ts +6 -1
  35. package/dist/src/execution/workflow-steps.js +76 -25
  36. package/dist/src/harness/execute-tool.d.ts +3 -3
  37. package/dist/src/harness/runtime-actions.d.ts +1 -0
  38. package/dist/src/harness/runtime-actions.js +18 -1
  39. package/dist/src/internal/application/package.js +1 -1
  40. package/dist/src/protocol/message.d.ts +6 -0
  41. package/dist/src/protocol/message.js +1 -0
  42. package/dist/src/protocol/routes.d.ts +11 -0
  43. package/dist/src/protocol/routes.js +13 -0
  44. package/dist/src/public/channels/ash.js +25 -1
  45. package/dist/src/runtime/actions/keys.js +2 -0
  46. package/dist/src/runtime/actions/types.d.ts +47 -1
  47. package/dist/src/runtime/actions/types.js +23 -0
  48. package/dist/src/runtime/connections/callback-route.d.ts +1 -1
  49. package/dist/src/runtime/connections/callback-route.js +1 -1
  50. package/dist/src/runtime/connections/mcp-client.d.ts +1 -2
  51. package/dist/src/runtime/connections/mcp-client.js +69 -3
  52. package/dist/src/runtime/framework-channels/index.js +7 -2
  53. package/dist/src/runtime/session-callback-route.d.ts +6 -0
  54. package/dist/src/runtime/session-callback-route.js +87 -0
  55. package/package.json +1 -1
  56. package/dist/src/chunks/host-BxT35q6K.js +0 -70
  57. package/dist/src/chunks/paths-B2hLA0Fn.js +0 -85
  58. package/dist/src/chunks/types-CjIyrcYo.js +0 -1
@@ -9,7 +9,7 @@
9
9
  * ({@link import("./keys.js").SessionKey SessionKey}, etc.) live in
10
10
  * `context/keys.ts` which re-exports everything from this module.
11
11
  */
12
- import type { SessionAuthContext, SessionCapabilities, SessionParent } from "#channel/types.js";
12
+ import type { SessionAuthContext, SessionCallback, SessionCapabilities, SessionParent } from "#channel/types.js";
13
13
  import type { RunMode } from "#shared/run-mode.js";
14
14
  import { ContextKey } from "#context/key.js";
15
15
  export declare const AuthKey: ContextKey<SessionAuthContext | null>;
@@ -24,6 +24,10 @@ export declare const ParentSessionKey: ContextKey<SessionParent>;
24
24
  * dispatch so HITL readiness flows through a conversation chain.
25
25
  */
26
26
  export declare const CapabilitiesKey: ContextKey<SessionCapabilities>;
27
+ /**
28
+ * Optional framework-owned terminal callback metadata for this session.
29
+ */
30
+ export declare const SessionCallbackKey: ContextKey<SessionCallback>;
27
31
  /**
28
32
  * Marker durable boolean set by the runtime **before** the
29
33
  * `lifecycle.session` hook chain runs. The runtime checks this flag at
@@ -22,6 +22,10 @@ export const ParentSessionKey = new ContextKey("ash.parentSession");
22
22
  * dispatch so HITL readiness flows through a conversation chain.
23
23
  */
24
24
  export const CapabilitiesKey = new ContextKey("ash.capabilities");
25
+ /**
26
+ * Optional framework-owned terminal callback metadata for this session.
27
+ */
28
+ export const SessionCallbackKey = new ContextKey("ash.sessionCallback");
25
29
  /**
26
30
  * Marker durable boolean set by the runtime **before** the
27
31
  * `lifecycle.session` hook chain runs. The runtime checks this flag at
@@ -1 +1 @@
1
- import{n as e}from"../../chunks/paths-B2hLA0Fn.js";import{loadDevelopmentEnvironmentFiles as t}from"../../cli/dev/environment.js";import{n,s as r,t as i}from"../../chunks/client-BShLWzR6.js";import{n as a}from"../../chunks/host-BxT35q6K.js";import{discoverAndImportSuites as o}from"../runner/discover.js";import{executeSuite as s}from"../runner/execute-suite.js";import{ConsoleReporter as c}from"../runner/reporters/console.js";var l=r();async function u(n,r){let i=e();t(i);let c=n.suite,l=await o(i,c);if(l.length===0){c&&c.length>0?r.error(`No suites found matching: ${c.join(`, `)}`):r.error(`No eval suites found. Create suite files under evals/ with the *.eval.ts extension.`),process.exitCode=1;return}let u,f;n.url?f={kind:`remote`,url:n.url}:(u=await a(i,{host:`127.0.0.1`,port:0}),f={kind:`local`,url:u.url});let p=d(f);try{let e=[];for(let t of l){let r=m(t,n),a=h(r,{json:n.json===!0,skipReport:n.skipReport===!0}),o=await s({suite:r,target:f,reporters:a,appRoot:i,client:p});e.push(o)}n.json&&r.log(JSON.stringify(e,null,2)),e.some(e=>e.errored>0)&&(process.exitCode=1)}finally{u&&await u.close()}process.exit(process.exitCode??0)}function d(e){if(e.kind===`local`)return new i({host:e.url});let t={},r=process.env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();return r&&(t[n]=r),new i({auth:f(),headers:Object.keys(t).length>0?t:void 0,host:e.url})}function f(){let e=process.env.ASH_EVAL_AUTH_TOKEN?.trim();return e?{bearer:e}:{bearer:p}}async function p(){try{let e=(await(0,l.getVercelOidcToken)()).trim();if(e.length>0)return e}catch{}return process.env.VERCEL_OIDC_TOKEN?.trim()??``}function m(e,t){let n=t.maxConcurrency?Number.parseInt(t.maxConcurrency,10):void 0,r=t.timeout?Number.parseInt(t.timeout,10):void 0;if(n===void 0&&r===void 0)return e;let i={...e};return n!==void 0&&(i.maxConcurrency=n),r!==void 0&&(i.timeoutMs=r),i}function h(e,t){let n=t.json?[]:[new c];return!t.skipReport&&e.reporters&&n.push(...e.reporters),n}export{u as runEvalCommand};
1
+ import{n as e}from"../../chunks/paths-DnlVBqHu.js";import{loadDevelopmentEnvironmentFiles as t}from"../../cli/dev/environment.js";import{n,s as r,t as i}from"../../chunks/client-ZqNLLMZB.js";import{n as a}from"../../chunks/host-F-DkwYJK.js";import{discoverAndImportSuites as o}from"../runner/discover.js";import{executeSuite as s}from"../runner/execute-suite.js";import{ConsoleReporter as c}from"../runner/reporters/console.js";var l=r();async function u(n,r){let i=e();t(i);let c=n.suite,l=await o(i,c);if(l.length===0){c&&c.length>0?r.error(`No suites found matching: ${c.join(`, `)}`):r.error(`No eval suites found. Create suite files under evals/ with the *.eval.ts extension.`),process.exitCode=1;return}let u,f;n.url?f={kind:`remote`,url:n.url}:(u=await a(i,{host:`127.0.0.1`,port:0}),f={kind:`local`,url:u.url});let p=d(f);try{let e=[];for(let t of l){let r=m(t,n),a=h(r,{json:n.json===!0,skipReport:n.skipReport===!0}),o=await s({suite:r,target:f,reporters:a,appRoot:i,client:p});e.push(o)}n.json&&r.log(JSON.stringify(e,null,2)),e.some(e=>e.errored>0)&&(process.exitCode=1)}finally{u&&await u.close()}process.exit(process.exitCode??0)}function d(e){if(e.kind===`local`)return new i({host:e.url});let t={},r=process.env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();return r&&(t[n]=r),new i({auth:f(),headers:Object.keys(t).length>0?t:void 0,host:e.url})}function f(){let e=process.env.ASH_EVAL_AUTH_TOKEN?.trim();return e?{bearer:e}:{bearer:p}}async function p(){try{let e=(await(0,l.getVercelOidcToken)()).trim();if(e.length>0)return e}catch{}return process.env.VERCEL_OIDC_TOKEN?.trim()??``}function m(e,t){let n=t.maxConcurrency?Number.parseInt(t.maxConcurrency,10):void 0,r=t.timeout?Number.parseInt(t.timeout,10):void 0;if(n===void 0&&r===void 0)return e;let i={...e};return n!==void 0&&(i.maxConcurrency=n),r!==void 0&&(i.timeoutMs=r),i}function h(e,t){let n=t.json?[]:[new c];return!t.skipReport&&e.reporters&&n.push(...e.reporters),n}export{u as runEvalCommand};
@@ -39,7 +39,7 @@ export async function awaitAuthorizationAndResolve(input) {
39
39
  // the IdP at Workflow's raw
40
40
  // `/.well-known/workflow/v1/webhook/:token` endpoint, we build a
41
41
  // namespaced Ash route at
42
- // `/.well-known/ash/v1/connections/:name/callback/:token`. The
42
+ // `/ash/v1/connections/:name/callback/:token`. The
43
43
  // matching framework route handler projects the inbound request and
44
44
  // resumes this hook via `resumeHook(token, payload)`. Owning the
45
45
  // callback in Ash lets the framework decide delivery policy (auth,
@@ -116,6 +116,19 @@ function resolveHarnessToolDefinition(input) {
116
116
  },
117
117
  };
118
118
  }
119
+ if (input.tool.kind === "remote") {
120
+ return {
121
+ description: input.tool.description ?? "",
122
+ inputSchema: jsonSchema(input.tool.inputSchema ?? {}),
123
+ name: input.tool.name,
124
+ runtimeAction: {
125
+ kind: "remote-agent-call",
126
+ nodeId: input.tool.nodeId,
127
+ remoteAgentName: input.tool.name,
128
+ subagentName: input.tool.name,
129
+ },
130
+ };
131
+ }
119
132
  const registeredTool = findRegisteredRuntimeTool(input.node.toolRegistry, input.tool.name);
120
133
  if (registeredTool === null) {
121
134
  return null;
@@ -0,0 +1,15 @@
1
+ import type { HarnessSession } from "#harness/types.js";
2
+ import type { RuntimeRemoteAgentCallActionRequest } from "#runtime/actions/types.js";
3
+ import type { RuntimeSubagentRegistry } from "#runtime/subagents/registry.js";
4
+ import type { ResolvedRuntimeRemoteAgentNode } from "#runtime/types.js";
5
+ export declare function startRemoteAgentSession(input: {
6
+ readonly action: RuntimeRemoteAgentCallActionRequest;
7
+ readonly callbackBaseUrl: string | undefined;
8
+ readonly remote: ResolvedRuntimeRemoteAgentNode;
9
+ readonly session: HarnessSession;
10
+ }): Promise<string>;
11
+ export declare function resolveRemoteAgentForAction(input: {
12
+ readonly nodeId: string;
13
+ readonly registry: RuntimeSubagentRegistry["subagentsByNodeId"];
14
+ readonly remoteAgentName: string;
15
+ }): ResolvedRuntimeRemoteAgentNode;
@@ -0,0 +1,79 @@
1
+ import { ASH_SESSION_ID_HEADER } from "#protocol/message.js";
2
+ import { createAshCallbackRoutePath } from "#protocol/routes.js";
3
+ import { formatSubagentInvocation } from "#execution/subagent-invocation.js";
4
+ export async function startRemoteAgentSession(input) {
5
+ const callbackToken = input.session.continuationToken;
6
+ if (!callbackToken) {
7
+ throw new Error("Cannot dispatch remote agent without a parent continuation token.");
8
+ }
9
+ if (!input.callbackBaseUrl) {
10
+ throw new Error("Cannot dispatch remote agent without a callback base URL.");
11
+ }
12
+ const headers = await resolveRemoteAgentRequestHeaders(input.remote);
13
+ const response = await fetch(createRemoteAgentSessionUrl(input.remote), {
14
+ body: JSON.stringify({
15
+ callback: {
16
+ callId: input.action.callId,
17
+ subagentName: input.action.remoteAgentName,
18
+ token: callbackToken,
19
+ url: `${input.callbackBaseUrl}${createAshCallbackRoutePath(callbackToken)}`,
20
+ },
21
+ message: formatRemoteAgentCallInputMessage(input.action),
22
+ mode: "task",
23
+ }),
24
+ headers: {
25
+ "content-type": "application/json",
26
+ ...headers,
27
+ },
28
+ method: "POST",
29
+ });
30
+ if (!response.ok) {
31
+ throw new Error(`Remote agent "${input.action.remoteAgentName}" create-session request failed with HTTP ${response.status}.`);
32
+ }
33
+ const sessionIdFromHeader = response.headers.get(ASH_SESSION_ID_HEADER);
34
+ if (sessionIdFromHeader !== null && sessionIdFromHeader.length > 0) {
35
+ return sessionIdFromHeader;
36
+ }
37
+ try {
38
+ const body = (await response.json());
39
+ if (typeof body.sessionId === "string" && body.sessionId.length > 0) {
40
+ return body.sessionId;
41
+ }
42
+ }
43
+ catch {
44
+ // Fall through to the generic error below.
45
+ }
46
+ throw new Error(`Remote agent "${input.action.remoteAgentName}" create-session response did not include a session id.`);
47
+ }
48
+ export function resolveRemoteAgentForAction(input) {
49
+ const registered = input.registry.get(input.nodeId);
50
+ const definition = registered?.definition;
51
+ if (definition?.kind !== "remote") {
52
+ throw new Error(`Missing remote agent "${input.remoteAgentName}" in runtime registry.`);
53
+ }
54
+ return definition;
55
+ }
56
+ function createRemoteAgentSessionUrl(remote) {
57
+ return new URL(remote.path, `${trimTrailingSlash(remote.url)}/`).toString();
58
+ }
59
+ async function resolveRemoteAgentRequestHeaders(remote) {
60
+ const headers = {};
61
+ if (remote.headers !== undefined) {
62
+ Object.assign(headers, typeof remote.headers === "function" ? await remote.headers() : remote.headers);
63
+ }
64
+ if (remote.auth !== undefined) {
65
+ Object.assign(headers, (await remote.auth()).headers);
66
+ }
67
+ return headers;
68
+ }
69
+ function formatRemoteAgentCallInputMessage(input) {
70
+ const message = typeof input.input.message === "string" ? input.input.message : "";
71
+ return formatSubagentInvocation({
72
+ description: input.description,
73
+ message,
74
+ name: input.remoteAgentName,
75
+ }).message;
76
+ }
77
+ function trimTrailingSlash(value) {
78
+ return value.endsWith("/") ? value.slice(0, -1) : value;
79
+ }
@@ -1,5 +1,5 @@
1
1
  import { ContextContainer } from "#context/container.js";
2
- import { AuthKey, BundleKey, CapabilitiesKey, ChannelKey, ContinuationTokenKey, InitiatorAuthKey, ModeKey, ParentSessionKey, } from "#context/keys.js";
2
+ import { AuthKey, BundleKey, CapabilitiesKey, ChannelKey, ContinuationTokenKey, InitiatorAuthKey, ModeKey, ParentSessionKey, SessionCallbackKey, } from "#context/keys.js";
3
3
  /**
4
4
  * Builds the bootstrap {@link ContextContainer} for one run.
5
5
  */
@@ -16,6 +16,9 @@ export function buildRunContext(input) {
16
16
  if (run.capabilities !== undefined) {
17
17
  ctx.set(CapabilitiesKey, run.capabilities);
18
18
  }
19
+ if (run.callback !== undefined) {
20
+ ctx.set(SessionCallbackKey, run.callback);
21
+ }
19
22
  if (run.parent !== undefined) {
20
23
  ctx.set(ParentSessionKey, run.parent);
21
24
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Sends the configured session terminal callback.
3
+ *
4
+ * Absence is a no-op. Once callback metadata is present, delivery is part of
5
+ * the remote delegation result path, so failures are logged and rethrown
6
+ * instead of being reported as a successful terminal step. Throwing is
7
+ * intentional: this function runs as a durable Workflow step, so rejection
8
+ * hands retry/failure policy back to the Workflow orchestrator rather than
9
+ * letting Ash falsely mark the callback delivery as complete.
10
+ */
11
+ export declare function fireSessionCallbackStep(input: {
12
+ readonly error?: unknown;
13
+ readonly output?: string;
14
+ readonly serializedContext: Record<string, unknown>;
15
+ readonly status: "completed" | "failed";
16
+ }): Promise<void>;
@@ -0,0 +1,72 @@
1
+ import { parseSessionCallback } from "#channel/session-callback.js";
2
+ import { SessionCallbackKey } from "#context/keys.js";
3
+ import { createLogger } from "#internal/logging.js";
4
+ import { toErrorMessage } from "#shared/errors.js";
5
+ const SESSION_CALLBACK_TIMEOUT_MS = 30_000;
6
+ const log = createLogger("execution.session-callback");
7
+ /**
8
+ * Sends the configured session terminal callback.
9
+ *
10
+ * Absence is a no-op. Once callback metadata is present, delivery is part of
11
+ * the remote delegation result path, so failures are logged and rethrown
12
+ * instead of being reported as a successful terminal step. Throwing is
13
+ * intentional: this function runs as a durable Workflow step, so rejection
14
+ * hands retry/failure policy back to the Workflow orchestrator rather than
15
+ * letting Ash falsely mark the callback delivery as complete.
16
+ */
17
+ export async function fireSessionCallbackStep(input) {
18
+ "use step";
19
+ const sessionId = input.serializedContext["ash.sessionId"] ?? "";
20
+ const value = input.serializedContext[SessionCallbackKey.name];
21
+ if (value === undefined) {
22
+ return;
23
+ }
24
+ try {
25
+ const callback = parseSerializedSessionCallback(value);
26
+ const body = input.status === "completed"
27
+ ? {
28
+ callId: callback.callId,
29
+ kind: "session.completed",
30
+ output: input.output ?? "",
31
+ sessionId,
32
+ subagentName: callback.subagentName,
33
+ }
34
+ : {
35
+ callId: callback.callId,
36
+ error: {
37
+ code: "SESSION_FAILED",
38
+ message: toErrorMessage(input.error),
39
+ },
40
+ kind: "session.failed",
41
+ sessionId,
42
+ subagentName: callback.subagentName,
43
+ };
44
+ const response = await fetch(callback.url, {
45
+ body: JSON.stringify(body),
46
+ headers: {
47
+ "content-type": "application/json",
48
+ },
49
+ method: "POST",
50
+ signal: AbortSignal.timeout(SESSION_CALLBACK_TIMEOUT_MS),
51
+ });
52
+ if (!response.ok) {
53
+ throw new Error(`Session callback failed with HTTP ${response.status}.`);
54
+ }
55
+ }
56
+ catch (error) {
57
+ log.error("failed to post session callback", {
58
+ error,
59
+ sessionId,
60
+ });
61
+ throw error;
62
+ }
63
+ }
64
+ function parseSerializedSessionCallback(value) {
65
+ const parsed = parseSessionCallback(value);
66
+ if (!parsed.ok) {
67
+ throw new Error("Serialized session callback is invalid.", {
68
+ cause: parsed.cause,
69
+ });
70
+ }
71
+ return parsed.callback;
72
+ }
@@ -0,0 +1,16 @@
1
+ import type { StepInput } from "#harness/types.js";
2
+ /**
3
+ * Narrowed form of {@link StepInput} whose `message` is always a plain string.
4
+ * Delegated child runs receive a synthesized text-only prompt.
5
+ */
6
+ export interface FormattedSubagentInvocation extends StepInput {
7
+ readonly message: string;
8
+ }
9
+ /**
10
+ * Formats the stable delegated input handed to one child agent invocation.
11
+ */
12
+ export declare function formatSubagentInvocation(input: {
13
+ readonly description: string;
14
+ readonly message: string;
15
+ readonly name: string;
16
+ }): FormattedSubagentInvocation;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Formats the stable delegated input handed to one child agent invocation.
3
+ */
4
+ export function formatSubagentInvocation(input) {
5
+ return {
6
+ message: [
7
+ `You are the subagent "${input.name}".`,
8
+ `Description: ${input.description}`,
9
+ "",
10
+ "The caller delegated the following task to you. Complete it and return the final result directly.",
11
+ "",
12
+ "Caller message:",
13
+ input.message,
14
+ ].join("\n"),
15
+ };
16
+ }
@@ -1,4 +1,5 @@
1
1
  import { SUBAGENT_ADAPTER_KIND } from "#execution/subagent-adapter.js";
2
+ import { formatSubagentInvocation } from "#execution/subagent-invocation.js";
2
3
  import { mintSubagentContinuationToken } from "#execution/session.js";
3
4
  /**
4
5
  * Builds the {@link RunInput} for one delegated subagent child run.
@@ -39,13 +40,9 @@ export function buildSubagentRunInput(input) {
39
40
  */
40
41
  function formatSubagentCallInputMessage(action) {
41
42
  const { message } = action.input;
42
- return [
43
- `You are the subagent "${action.subagentName}".`,
44
- `Description: ${action.description}`,
45
- "",
46
- "The caller delegated the following task to you. Complete it and return the final result directly.",
47
- "",
48
- "Caller message:",
43
+ return formatSubagentInvocation({
44
+ description: action.description,
49
45
  message,
50
- ].join("\n");
46
+ name: action.subagentName,
47
+ }).message;
51
48
  }
@@ -8,6 +8,7 @@ import { accumulateRuntimeActionResults, hasPendingRuntimeActionBatch, } from "#
8
8
  import { toErrorMessage } from "#shared/errors.js";
9
9
  import { normalizeSerializableError, rebuildSerializableError, } from "#execution/workflow-errors.js";
10
10
  import { createSessionStep, dispatchTurnStep, dispatchPendingRuntimeActionsStep, emitTerminalSessionFailureStep, routeProxiedDeliverStep, runProxyInputRequestStep, } from "#execution/workflow-steps.js";
11
+ import { fireSessionCallbackStep } from "#execution/session-callback-step.js";
11
12
  /**
12
13
  * Long-lived workflow entrypoint for the durable runtime.
13
14
  *
@@ -64,6 +65,11 @@ export async function workflowEntry(input) {
64
65
  parentWritable: driverWritable,
65
66
  serializedContext: input.serializedContext,
66
67
  });
68
+ await fireSessionCallbackStep({
69
+ error: normalizeSerializableError(error),
70
+ serializedContext: input.serializedContext,
71
+ status: "failed",
72
+ });
67
73
  await notifyDelegatedParentStep({
68
74
  result: createDelegatedSubagentErrorResult(input.serializedContext, error),
69
75
  serializedContext: input.serializedContext,
@@ -88,6 +94,11 @@ async function runDriverLoop(input) {
88
94
  currentSession = currentResult.session;
89
95
  currentSerializedContext = currentResult.serializedContext;
90
96
  if (currentResult.action === "done") {
97
+ await fireSessionCallbackStep({
98
+ output: currentResult.output ?? "",
99
+ serializedContext: currentSerializedContext,
100
+ status: "completed",
101
+ });
91
102
  await notifyDelegatedParentStep({
92
103
  result: createDelegatedSubagentSuccessResult(currentSerializedContext, currentResult.output ?? ""),
93
104
  serializedContext: currentSerializedContext,
@@ -138,15 +149,18 @@ async function runDriverLoop(input) {
138
149
  try {
139
150
  while (true) {
140
151
  if (hasPendingRuntimeActionBatch(currentSession)) {
141
- currentSession = await dispatchPendingRuntimeActionsStep({
152
+ const dispatchResult = await dispatchPendingRuntimeActionsStep({
153
+ callbackBaseUrl: getWorkflowMetadata().url.replace(/\/$/, ""),
142
154
  parentWritable: input.driverWritable,
143
155
  serializedContext: currentSerializedContext,
144
156
  session: currentSession,
145
157
  });
158
+ currentSession = dispatchResult.session;
146
159
  const runtimeResults = await waitForPendingRuntimeActionResults({
147
160
  bufferedDeliveries,
148
161
  getNextPromise,
149
162
  consumeNext,
163
+ initialResults: dispatchResult.results,
150
164
  rekeyHook,
151
165
  parentWritable: input.driverWritable,
152
166
  serializedContext: currentSerializedContext,
@@ -207,6 +221,11 @@ async function runDriverLoop(input) {
207
221
  currentSession = currentResult.session;
208
222
  currentSerializedContext = currentResult.serializedContext;
209
223
  if (currentResult.action === "done") {
224
+ await fireSessionCallbackStep({
225
+ output: currentResult.output ?? "",
226
+ serializedContext: currentSerializedContext,
227
+ status: "completed",
228
+ });
210
229
  await notifyDelegatedParentStep({
211
230
  result: createDelegatedSubagentSuccessResult(currentSerializedContext, currentResult.output ?? ""),
212
231
  serializedContext: currentSerializedContext,
@@ -309,6 +328,7 @@ async function waitForPendingRuntimeActionResults(input) {
309
328
  await input.rekeyHook(currentSession.continuationToken);
310
329
  }
311
330
  },
331
+ initialResults: input.initialResults,
312
332
  session: currentSession,
313
333
  });
314
334
  if (results === null) {
@@ -4,6 +4,7 @@ import type { HarnessSession } from "#harness/types.js";
4
4
  import type { RuntimeCompiledArtifactsSource } from "#runtime/compiled-artifacts-source.js";
5
5
  import { type PendingConnectionToolCall } from "#runtime/framework-tools/pending-connection-tool-calls.js";
6
6
  import { type TurnWorkflowInput } from "#execution/turn-workflow.js";
7
+ import type { RuntimeSubagentResultActionResult } from "#runtime/actions/types.js";
7
8
  /**
8
9
  * Serializable projection of a step result for workflow persistence.
9
10
  */
@@ -55,10 +56,14 @@ export declare function reconcileSessionContinuationToken(ctx: Awaited<ReturnTyp
55
56
  * runs independently on its own public child stream.
56
57
  */
57
58
  export declare function dispatchPendingRuntimeActionsStep(input: {
59
+ readonly callbackBaseUrl?: string;
58
60
  readonly parentWritable: WritableStream<Uint8Array>;
59
61
  readonly serializedContext: Record<string, unknown>;
60
62
  readonly session: HarnessSession;
61
- }): Promise<HarnessSession>;
63
+ }): Promise<{
64
+ readonly results: readonly RuntimeSubagentResultActionResult[];
65
+ readonly session: HarnessSession;
66
+ }>;
62
67
  /**
63
68
  * Emits a terminal `session.failed` event and delivers it to the
64
69
  * adapter before the workflow run tears down.
@@ -13,11 +13,13 @@ import { createSessionFailedEvent, createSubagentCalledEvent, encodeMessageStrea
13
13
  import { PendingConnectionToolCallsKey, } from "#runtime/framework-tools/pending-connection-tool-calls.js";
14
14
  import { getCompiledRuntimeAgentBundle } from "#runtime/sessions/compiled-agent-cache.js";
15
15
  import { createExecutionNodeStep } from "#execution/node-step.js";
16
+ import { resolveRemoteAgentForAction, startRemoteAgentSession, } from "#execution/remote-agent-dispatch.js";
16
17
  import { emitProxiedInputRequest, routeDeliverPayload } from "#execution/subagent-hitl-proxy.js";
17
18
  import { createSession, refreshSessionFromTurnAgent } from "#execution/session.js";
18
19
  import { buildSubagentRunInput } from "#execution/subagent-tool.js";
19
20
  import { turnWorkflow } from "#execution/turn-workflow.js";
20
21
  import { createWorkflowRuntime, startWorkflowPreferLatest, workflowEntryReference, } from "#execution/workflow-runtime.js";
22
+ import { toErrorMessage } from "#shared/errors.js";
21
23
  /**
22
24
  * Runs one atomic harness step inside a durable `"use step"` boundary.
23
25
  */
@@ -201,7 +203,7 @@ export async function dispatchPendingRuntimeActionsStep(input) {
201
203
  "use step";
202
204
  const batch = getPendingRuntimeActionBatch(input.session);
203
205
  if (batch === undefined || batch.actions.length === 0) {
204
- return input.session;
206
+ return { results: [], session: input.session };
205
207
  }
206
208
  const ctx = await deserializeContext(input.serializedContext);
207
209
  const bundle = ctx.require(BundleKey);
@@ -212,36 +214,73 @@ export async function dispatchPendingRuntimeActionsStep(input) {
212
214
  const writer = input.parentWritable.getWriter();
213
215
  const adapterCtx = buildAdapterContext(adapter, ctx);
214
216
  let nextSession = input.session;
217
+ const results = [];
215
218
  try {
216
219
  for (const action of batch.actions) {
217
- if (action.kind !== "subagent-call") {
218
- throw new Error(`Unsupported runtime action kind "${action.kind}" in workflow runtime.`);
220
+ let childSessionId;
221
+ let name;
222
+ let remote;
223
+ let toolName;
224
+ switch (action.kind) {
225
+ case "subagent-call": {
226
+ const childRuntime = createWorkflowRuntime({
227
+ compiledArtifactsSource: bundle.compiledArtifactsSource,
228
+ nodeId: action.nodeId,
229
+ });
230
+ const { childContinuationToken, runInput } = buildSubagentRunInput({
231
+ action,
232
+ auth,
233
+ batchEvent: batch.event,
234
+ capabilities,
235
+ initiatorAuth,
236
+ session: input.session,
237
+ });
238
+ const handle = await childRuntime.run(runInput);
239
+ nextSession = recordPendingSubagentChildToken({
240
+ callId: action.callId,
241
+ childContinuationToken,
242
+ session: nextSession,
243
+ });
244
+ childSessionId = handle.sessionId;
245
+ name = action.name;
246
+ toolName = action.subagentName;
247
+ break;
248
+ }
249
+ case "remote-agent-call": {
250
+ let resolvedRemote;
251
+ try {
252
+ resolvedRemote = resolveRemoteAgentForAction({
253
+ nodeId: action.nodeId,
254
+ remoteAgentName: action.remoteAgentName,
255
+ registry: bundle.subagentRegistry.subagentsByNodeId,
256
+ });
257
+ childSessionId = await startRemoteAgentSession({
258
+ action,
259
+ callbackBaseUrl: input.callbackBaseUrl,
260
+ remote: resolvedRemote,
261
+ session: input.session,
262
+ });
263
+ }
264
+ catch (error) {
265
+ results.push(createRemoteAgentStartFailureResult({ action, error }));
266
+ continue;
267
+ }
268
+ name = action.name;
269
+ remote = { url: resolvedRemote.url };
270
+ toolName = action.remoteAgentName;
271
+ break;
272
+ }
273
+ default:
274
+ throw new Error(`Unsupported runtime action kind "${action.kind}" in workflow runtime.`);
219
275
  }
220
- const childRuntime = createWorkflowRuntime({
221
- compiledArtifactsSource: bundle.compiledArtifactsSource,
222
- nodeId: action.nodeId,
223
- });
224
- const { childContinuationToken, runInput } = buildSubagentRunInput({
225
- action,
226
- auth,
227
- batchEvent: batch.event,
228
- capabilities,
229
- initiatorAuth,
230
- session: input.session,
231
- });
232
- const handle = await childRuntime.run(runInput);
233
- nextSession = recordPendingSubagentChildToken({
234
- callId: action.callId,
235
- childContinuationToken,
236
- session: nextSession,
237
- });
238
276
  const parentEvent = await callAdapterEventHandler(adapter, createSubagentCalledEvent({
239
277
  callId: action.callId,
240
- childSessionId: handle.sessionId,
241
- name: action.name,
278
+ childSessionId,
279
+ name,
280
+ remote,
242
281
  sequence: batch.event.sequence,
243
282
  sessionId: input.session.sessionId,
244
- toolName: action.subagentName,
283
+ toolName,
245
284
  turnId: batch.event.turnId,
246
285
  workflowId: workflowEntryReference.workflowId,
247
286
  }), adapterCtx);
@@ -251,7 +290,19 @@ export async function dispatchPendingRuntimeActionsStep(input) {
251
290
  finally {
252
291
  writer.releaseLock();
253
292
  }
254
- return nextSession;
293
+ return { results, session: nextSession };
294
+ }
295
+ function createRemoteAgentStartFailureResult(input) {
296
+ return {
297
+ callId: input.action.callId,
298
+ isError: true,
299
+ kind: "subagent-result",
300
+ output: {
301
+ code: "REMOTE_AGENT_START_FAILED",
302
+ message: toErrorMessage(input.error),
303
+ },
304
+ subagentName: input.action.remoteAgentName,
305
+ };
255
306
  }
256
307
  const log = createLogger("execution.workflow-entry");
257
308
  /**
@@ -6,9 +6,10 @@ import type { NeedsApprovalContext } from "#public/definitions/tool.js";
6
6
  * These tools are surfaced to the model without a local `execute` function.
7
7
  * The harness records the tool call and the runtime executes it later.
8
8
  */
9
- type HarnessRuntimeActionDefinition = {
10
- readonly kind: "subagent-call";
9
+ export type HarnessRuntimeActionDefinition = {
10
+ readonly kind: "remote-agent-call" | "subagent-call";
11
11
  readonly nodeId: string;
12
+ readonly remoteAgentName?: string;
12
13
  readonly subagentName: string;
13
14
  };
14
15
  /**
@@ -24,4 +25,3 @@ export interface HarnessToolDefinition {
24
25
  readonly runtimeAction?: HarnessRuntimeActionDefinition;
25
26
  readonly toModelOutput?: (output: unknown) => unknown;
26
27
  }
27
- export {};
@@ -86,6 +86,7 @@ type RuntimeActionAccumulatorItem<TDeliver> = {
86
86
  export declare function accumulateRuntimeActionResults<TDeliver>(input: {
87
87
  readonly bufferedDeliveries: TDeliver[];
88
88
  readonly getNext: () => Promise<RuntimeActionAccumulatorItem<TDeliver> | null>;
89
+ readonly initialResults?: readonly RuntimeActionResult[];
89
90
  readonly session: HarnessSession;
90
91
  }): Promise<RuntimeActionResult[] | null>;
91
92
  /**
@@ -65,7 +65,13 @@ export function recordPendingSubagentChildToken(input) {
65
65
  */
66
66
  export async function accumulateRuntimeActionResults(input) {
67
67
  const batch = getPendingRuntimeActionBatch(input.session);
68
- const buffered = [];
68
+ const buffered = [...(input.initialResults ?? [])];
69
+ if (batch !== undefined && buffered.length > 0) {
70
+ const ready = resolveRuntimeActionResultsForBatch({ batch, results: buffered });
71
+ if (ready !== undefined) {
72
+ return ready;
73
+ }
74
+ }
69
75
  while (true) {
70
76
  const item = await input.getNext();
71
77
  if (item === null) {
@@ -249,6 +255,17 @@ export function createRuntimeActionRequestFromToolCall(input) {
249
255
  subagentName: definition.runtimeAction.subagentName,
250
256
  };
251
257
  }
258
+ if (definition?.runtimeAction?.kind === "remote-agent-call") {
259
+ return {
260
+ callId: input.toolCall.toolCallId,
261
+ description: definition.description,
262
+ input: resolveToolCallInputObject(input.toolCall.input),
263
+ kind: "remote-agent-call",
264
+ name: definition.name,
265
+ nodeId: definition.runtimeAction.nodeId,
266
+ remoteAgentName: definition.runtimeAction.remoteAgentName ?? definition.name,
267
+ };
268
+ }
252
269
  return {
253
270
  callId: input.toolCall.toolCallId,
254
271
  input: resolveToolCallInputObject(input.toolCall.input),
@@ -6,7 +6,7 @@ import { ASH_PACKAGE_NAME } from "#internal/package-name.js";
6
6
  let cachedPackageInfo;
7
7
  // The package build stamps the published version into `dist` so bundled
8
8
  // deployments can still report package metadata without resolving package.json.
9
- const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.25.1";
9
+ const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.25.2";
10
10
  const BUNDLED_FALLBACK_PACKAGE_VERSION_PLACEHOLDER = "__ASH_PACKAGE_VERSION_PLACEHOLDER__";
11
11
  const WORKFLOW_MODULE_ALIASES = {
12
12
  "workflow/api": "src/compiled/@workflow/core/runtime.js",