@stigmer/runner 3.2.2 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +5 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +27 -2
- package/dist/activities/execute-cursor/approval-state.js +13 -1
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/blueprint-resolver.d.ts +9 -1
- package/dist/activities/execute-cursor/blueprint-resolver.js +1 -0
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/hook-script.js +38 -0
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +2 -0
- package/dist/activities/execute-cursor/index.js +56 -13
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +26 -0
- package/dist/activities/execute-cursor/message-translator.js +78 -2
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +7 -1
- package/dist/activities/execute-cursor/prompt-builder.js +4 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/session-lifecycle.d.ts +45 -0
- package/dist/activities/execute-cursor/session-lifecycle.js +41 -0
- package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
- package/dist/activities/execute-cursor/turn-boundary.js +15 -2
- package/dist/activities/execute-cursor/turn-boundary.js.map +1 -1
- package/dist/activities/execute-deep-agent/hitl.d.ts +25 -0
- package/dist/activities/execute-deep-agent/hitl.js +48 -1
- package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +10 -1
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +5 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +3 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +14 -0
- package/dist/activities/execute-deep-agent/setup.js +36 -3
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +8 -0
- package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.js +6 -0
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -0
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/activities/workflow-event-activities.js +9 -0
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/middleware/approval-gate.d.ts +20 -0
- package/dist/middleware/approval-gate.js +16 -1
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/runner-manager.js +3 -0
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +3 -0
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +24 -1
- package/dist/shared/approval-policy.js +35 -1
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/datastore-attachment.d.ts +79 -0
- package/dist/shared/datastore-attachment.js +129 -0
- package/dist/shared/datastore-attachment.js.map +1 -0
- package/dist/shared/with-timeout.d.ts +10 -0
- package/dist/shared/with-timeout.js +14 -1
- package/dist/shared/with-timeout.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.js +1 -0
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +22 -0
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/human-input-orchestrator.d.ts +2 -1
- package/dist/workflows/human-input-orchestrator.js +2 -1
- package/dist/workflows/human-input-orchestrator.js.map +1 -1
- package/package.json +2 -2
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +29 -0
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +6 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +25 -0
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +105 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +54 -0
- package/src/activities/execute-cursor/__tests__/session-lifecycle.test.ts +122 -1
- package/src/activities/execute-cursor/approval-state.ts +32 -1
- package/src/activities/execute-cursor/blueprint-resolver.ts +10 -1
- package/src/activities/execute-cursor/hook-script.ts +40 -0
- package/src/activities/execute-cursor/index.ts +64 -15
- package/src/activities/execute-cursor/message-translator.ts +82 -1
- package/src/activities/execute-cursor/prompt-builder.ts +12 -1
- package/src/activities/execute-cursor/session-lifecycle.ts +76 -0
- package/src/activities/execute-cursor/turn-boundary.ts +23 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +103 -2
- package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/hitl.ts +51 -0
- package/src/activities/execute-deep-agent/index.ts +11 -1
- package/src/activities/execute-deep-agent/prompt-builder.ts +9 -0
- package/src/activities/execute-deep-agent/setup.ts +57 -2
- package/src/activities/execute-deep-agent/status-builder-shared.ts +8 -0
- package/src/activities/execute-deep-agent/status-builder.ts +7 -0
- package/src/activities/execute-deep-agent/v3-status-builder.ts +7 -0
- package/src/activities/workflow-event-activities.ts +9 -0
- package/src/config.ts +11 -0
- package/src/middleware/__tests__/approval-gate.test.ts +95 -0
- package/src/middleware/approval-gate.ts +37 -0
- package/src/runner-manager.ts +3 -0
- package/src/runner.ts +3 -0
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/datastore-attachment.test.ts +165 -0
- package/src/shared/__tests__/with-timeout.test.ts +16 -1
- package/src/shared/approval-policy.ts +41 -2
- package/src/shared/datastore-attachment.ts +166 -0
- package/src/shared/with-timeout.ts +15 -1
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +40 -0
- package/src/workflow-engine/tasks/human-input.ts +1 -0
- package/src/workflow-engine/types.ts +23 -0
- package/src/workflows/human-input-orchestrator.ts +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { withTimeout } from "../with-timeout.js";
|
|
2
|
+
import { withTimeout, TimeoutError } from "../with-timeout.js";
|
|
3
3
|
|
|
4
4
|
describe("withTimeout", () => {
|
|
5
5
|
it("resolves with the function's result when it completes in time", async () => {
|
|
@@ -14,6 +14,21 @@ describe("withTimeout", () => {
|
|
|
14
14
|
).rejects.toThrow("Cursor agent create timed out after 20ms");
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
+
it("rejects with a TimeoutError so callers can react to expiry by type", async () => {
|
|
18
|
+
const hang = () => new Promise<never>(() => {});
|
|
19
|
+
await expect(withTimeout(20, "expired", hang)).rejects.toBeInstanceOf(TimeoutError);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("does not wrap the function's own rejection in a TimeoutError", async () => {
|
|
23
|
+
const boom = new Error("underlying failure");
|
|
24
|
+
let caught: unknown;
|
|
25
|
+
await withTimeout(1_000, "should not fire", () => Promise.reject(boom)).catch((err) => {
|
|
26
|
+
caught = err;
|
|
27
|
+
});
|
|
28
|
+
expect(caught).toBe(boom);
|
|
29
|
+
expect(caught).not.toBeInstanceOf(TimeoutError);
|
|
30
|
+
});
|
|
31
|
+
|
|
17
32
|
it("evaluates a lazy message only on expiry", async () => {
|
|
18
33
|
let evaluated = 0;
|
|
19
34
|
const message = () => {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import type { ToolApprovalPolicy } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
|
|
19
19
|
import type { ToolApprovalOverride } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
20
20
|
import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
21
|
-
import { ApprovalAction, ApprovalPolicySource } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
21
|
+
import { ApprovalAction, ApprovalMode, ApprovalPolicySource } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
22
22
|
import { toolApprovalCategory, type ToolApprovalCategory } from "./tool-kind.js";
|
|
23
23
|
import type { ResolvedMcpServer } from "./mcp-resolver.js";
|
|
24
24
|
|
|
@@ -147,6 +147,42 @@ export function deriveActiveLeases(execution: AgentExecution): ActiveLeases {
|
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Whether this execution runs in UNATTENDED approval mode
|
|
152
|
+
* (ExecutionConfig.approval_mode): the creating surface — a messaging
|
|
153
|
+
* channel, a guest share — has no approver, so a gated tool is resolved as
|
|
154
|
+
* an automatic skip (the model is told to adapt) instead of pausing the
|
|
155
|
+
* execution for a decision that can never arrive.
|
|
156
|
+
*
|
|
157
|
+
* The mode changes HOW a gate resolves, never WHAT is gated: the four-level
|
|
158
|
+
* policy merge below is identical in both modes. Both harnesses read the
|
|
159
|
+
* mode through this one helper (the native gate skips instead of
|
|
160
|
+
* interrupting; the Cursor hook records a non-pausing "unattended" denial),
|
|
161
|
+
* so the surfaces can never diverge on what "unattended" means.
|
|
162
|
+
*/
|
|
163
|
+
export function isUnattendedApprovalMode(execution: AgentExecution): boolean {
|
|
164
|
+
return execution.spec?.executionConfig?.approvalMode === ApprovalMode.UNATTENDED;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The tool-result text for an unattended auto-skip — ONE definition for both
|
|
169
|
+
* harnesses (the native gate returns it as the skip ToolMessage; the Cursor
|
|
170
|
+
* turn boundary backfills it onto stamped SKIPPED rows), so the transcript
|
|
171
|
+
* reads identically wherever the skip happened. Deliberately instructs
|
|
172
|
+
* plain-language adaptation with NO tool/approval vocabulary reaching the end
|
|
173
|
+
* user (the channel/guest anti-leak posture).
|
|
174
|
+
*/
|
|
175
|
+
export function unattendedSkipMessage(toolName: string): string {
|
|
176
|
+
return (
|
|
177
|
+
`Tool '${toolName}' requires an approval that is not available in ` +
|
|
178
|
+
`this conversation, so it was skipped automatically. Do not retry ` +
|
|
179
|
+
`it or attempt a workaround. Adapt your plan, and explain to the ` +
|
|
180
|
+
`user in plain language what you could not do and what they can ` +
|
|
181
|
+
`do instead — never mention tools, approvals, or platform ` +
|
|
182
|
+
`mechanics.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
150
186
|
/**
|
|
151
187
|
* Provenance of a gate decision: which policy layer (or decision point) is
|
|
152
188
|
* responsible for the final requires-approval verdict.
|
|
@@ -170,7 +206,8 @@ export type PolicySource =
|
|
|
170
206
|
| "approval_lease" // Layer 4: a run-lifetime scoped lease cleared this action
|
|
171
207
|
| "builtin_category" // Non-MCP built-in gated by the shared tool taxonomy
|
|
172
208
|
| "file_capture" // Capture mode: a git-tracked built-in file edit flows, reviewed post-hoc via the file_review ledger (not gated; audit-only on the shadow receipt)
|
|
173
|
-
| "annotation_destructive_tighten"
|
|
209
|
+
| "annotation_destructive_tighten" // Layer 1 sub-case: connect-time destructiveHint tightener force-gated this MCP tool
|
|
210
|
+
| "unattended_skip"; // Layer 4 resolution: unattended approval mode auto-skipped this gated call (no approver on the creating surface)
|
|
174
211
|
|
|
175
212
|
/**
|
|
176
213
|
* Monotonic identifier of the policy-engine logic that produced a decision,
|
|
@@ -205,6 +242,8 @@ export function toProtoPolicySource(source: PolicySource | undefined): ApprovalP
|
|
|
205
242
|
return ApprovalPolicySource.BUILTIN_CATEGORY;
|
|
206
243
|
case "annotation_destructive_tighten":
|
|
207
244
|
return ApprovalPolicySource.ANNOTATION_DESTRUCTIVE_TIGHTEN;
|
|
245
|
+
case "unattended_skip":
|
|
246
|
+
return ApprovalPolicySource.UNATTENDED_SKIP;
|
|
208
247
|
case "file_capture":
|
|
209
248
|
// Capture-mode flow is never persisted on a gated tool call (the file tool
|
|
210
249
|
// is not gated — it has no WAITING_APPROVAL row); it exists only on the
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runner-synthesized datastore records attachment (T05, DD-001 SD-2).
|
|
3
|
+
*
|
|
4
|
+
* When an agent declares `datastore_usages`, the runner synthesizes ONE
|
|
5
|
+
* MCP attachment serving the five record tools (DD-005) — the agent
|
|
6
|
+
* builder writes a usage line and gets tools; no McpServer resource, no
|
|
7
|
+
* Environment, no credential in any manifest. Harness-agnostic like
|
|
8
|
+
* mcp-resolver.ts: both ExecuteCursor and ExecuteDeepAgent inject the
|
|
9
|
+
* synthesized entry through their existing ResolvedMcpServer paths.
|
|
10
|
+
*
|
|
11
|
+
* Two connection shapes, one roster (the bridge's records-only roster,
|
|
12
|
+
* T05 R1):
|
|
13
|
+
* - Bridge endpoint configured (cloud): Streamable HTTP against the
|
|
14
|
+
* bridge's /records route, with the execution's own session-scoped
|
|
15
|
+
* credential as the Bearer token — the server's reach chain (DD-006
|
|
16
|
+
* Path 1) authorizes from that token; the bridge stays a
|
|
17
|
+
* non-validating passthrough.
|
|
18
|
+
* - No bridge endpoint (OSS/local): a spawned `stigmer mcp-server`
|
|
19
|
+
* stdio child with STIGMER_MCP_ROSTER=records against the local
|
|
20
|
+
* backend (zero new distribution — the CLI already embeds the
|
|
21
|
+
* bridge). No credential: the local backend is unauthenticated and
|
|
22
|
+
* OSS callers resolve as the local principal (T05 R2).
|
|
23
|
+
*
|
|
24
|
+
* Approval-free by construction (DD-001 SD-3): empty toolApprovals +
|
|
25
|
+
* pinnedToolApprovals mean mergeApprovalPolicies emits no entries for
|
|
26
|
+
* this server — zero classifier involvement. `discoveredCapabilitiesEmpty`
|
|
27
|
+
* is false and the attachment has no McpServerUsage, so the connect
|
|
28
|
+
* backfill (whose destructiveHint tightener would force-gate
|
|
29
|
+
* delete_record — silently skipped on channels under UNATTENDED mode)
|
|
30
|
+
* is structurally unable to touch it. Callers must still inject AFTER
|
|
31
|
+
* resolve + backfill; both harness call sites do.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { DatastoreUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
35
|
+
import type { ResolvedMcpServer } from "./mcp-resolver.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The synthesized attachment's slug. Reserved: a user McpServer with
|
|
39
|
+
* this slug is shadowed by the synthesized attachment (the record tools
|
|
40
|
+
* must exist whenever `datastore_usages` says so), with a warning.
|
|
41
|
+
*/
|
|
42
|
+
export const DATASTORE_ATTACHMENT_SLUG = "stigmer-records";
|
|
43
|
+
|
|
44
|
+
/** The bridge route serving the records-only roster (mcp-server T05 R1). */
|
|
45
|
+
export const RECORDS_ROUTE = "/records";
|
|
46
|
+
|
|
47
|
+
export interface DatastoreAttachmentOptions {
|
|
48
|
+
/**
|
|
49
|
+
* The bridge's HTTP endpoint (STIGMER_MCP_BRIDGE_ENDPOINT, e.g.
|
|
50
|
+
* https://mcp.stigmer.ai). Null selects the OSS stdio shape.
|
|
51
|
+
*/
|
|
52
|
+
bridgeEndpoint: string | null;
|
|
53
|
+
/**
|
|
54
|
+
* The execution's session-scoped credential (the sandbox token a
|
|
55
|
+
* cloud runner holds, or the desktop runner's exchanged scoped
|
|
56
|
+
* token). Null attaches no Authorization header (OSS/local).
|
|
57
|
+
*/
|
|
58
|
+
credential: string | null;
|
|
59
|
+
/**
|
|
60
|
+
* The stigmer backend endpoint the stdio child dials
|
|
61
|
+
* (config.stigmerBackendEndpoint). Only used for the OSS shape.
|
|
62
|
+
*/
|
|
63
|
+
backendEndpoint: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Synthesize the records attachment for an agent's datastore usages.
|
|
68
|
+
* Returns undefined when the agent uses no datastores — the attachment
|
|
69
|
+
* exists exactly when the usage edge does.
|
|
70
|
+
*/
|
|
71
|
+
export function synthesizeDatastoreAttachment(
|
|
72
|
+
datastoreUsages: DatastoreUsage[],
|
|
73
|
+
options: DatastoreAttachmentOptions,
|
|
74
|
+
): ResolvedMcpServer | undefined {
|
|
75
|
+
if (datastoreUsages.length === 0) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Approval-free by construction + backfill-proof: see file header.
|
|
80
|
+
const base = {
|
|
81
|
+
slug: DATASTORE_ATTACHMENT_SLUG,
|
|
82
|
+
toolApprovals: [],
|
|
83
|
+
pinnedToolApprovals: [],
|
|
84
|
+
discoveredCapabilitiesEmpty: false,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (options.bridgeEndpoint !== null && options.bridgeEndpoint !== "") {
|
|
88
|
+
return {
|
|
89
|
+
...base,
|
|
90
|
+
connectionType: "http",
|
|
91
|
+
url: options.bridgeEndpoint.replace(/\/+$/, "") + RECORDS_ROUTE,
|
|
92
|
+
headers: options.credential !== null && options.credential !== ""
|
|
93
|
+
? { Authorization: `Bearer ${options.credential}` }
|
|
94
|
+
: undefined,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
...base,
|
|
100
|
+
connectionType: "stdio",
|
|
101
|
+
command: "stigmer",
|
|
102
|
+
args: ["mcp-server"],
|
|
103
|
+
env: {
|
|
104
|
+
STIGMER_MCP_ROSTER: "records",
|
|
105
|
+
STIGMER_SERVER_ADDRESS: grpcTarget(options.backendEndpoint),
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Inject the synthesized attachment into a resolved server list —
|
|
112
|
+
* AFTER resolve + backfill (see file header). A user server shadowing
|
|
113
|
+
* the reserved slug is replaced, loudly.
|
|
114
|
+
*/
|
|
115
|
+
export function injectDatastoreAttachment(
|
|
116
|
+
resolvedServers: ResolvedMcpServer[],
|
|
117
|
+
attachment: ResolvedMcpServer,
|
|
118
|
+
): ResolvedMcpServer[] {
|
|
119
|
+
const shadowed = resolvedServers.some((s) => s.slug === attachment.slug);
|
|
120
|
+
if (shadowed) {
|
|
121
|
+
console.warn(
|
|
122
|
+
`MCP server slug "${attachment.slug}" is reserved for the datastore ` +
|
|
123
|
+
"records attachment; the user-defined server is replaced.",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return [
|
|
127
|
+
...resolvedServers.filter((s) => s.slug !== attachment.slug),
|
|
128
|
+
attachment,
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The `<available_datastores>` prompt section (DD-005 SD-5, the
|
|
134
|
+
* skills-section precedent): names the attached datastores and points
|
|
135
|
+
* the model at describe_datastore first. Shared by both harnesses so
|
|
136
|
+
* the section text cannot drift (the sender-identity precedent).
|
|
137
|
+
*/
|
|
138
|
+
export function formatDatastoresSection(datastoreUsages: DatastoreUsage[]): string {
|
|
139
|
+
const slugs = datastoreUsages
|
|
140
|
+
.map((u) => u.datastoreRef?.slug)
|
|
141
|
+
.filter((s): s is string => !!s);
|
|
142
|
+
const entries = slugs.map((slug) => `- ${slug}`);
|
|
143
|
+
return [
|
|
144
|
+
"<available_datastores>",
|
|
145
|
+
"You have access to the following datastores through the record tools",
|
|
146
|
+
"(describe_datastore, find_records, insert_record, update_record, delete_record).",
|
|
147
|
+
"Before your first operation against a datastore, call describe_datastore to",
|
|
148
|
+
"learn its collections, field encodings, and which operations you are allowed",
|
|
149
|
+
"to perform.",
|
|
150
|
+
"",
|
|
151
|
+
...entries,
|
|
152
|
+
"</available_datastores>",
|
|
153
|
+
].join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The gRPC dial target (host:port) for a backend endpoint URL — the
|
|
158
|
+
* shape STIGMER_SERVER_ADDRESS wants (the bridge warns on schemes).
|
|
159
|
+
*/
|
|
160
|
+
function grpcTarget(endpoint: string): string {
|
|
161
|
+
try {
|
|
162
|
+
return new URL(endpoint).host;
|
|
163
|
+
} catch {
|
|
164
|
+
return endpoint;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -15,6 +15,20 @@
|
|
|
15
15
|
* `AbortSignal.timeout` and pass the signal to the callee instead.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Thrown when the bound expires before `fn` settles. A distinct class so
|
|
20
|
+
* callers can react to expiry specifically (`err instanceof TimeoutError`)
|
|
21
|
+
* instead of sniffing message text — e.g. the agent-resolution transport
|
|
22
|
+
* recovery retries on expiry but must let deterministic failures (auth,
|
|
23
|
+
* validation) propagate untouched.
|
|
24
|
+
*/
|
|
25
|
+
export class TimeoutError extends Error {
|
|
26
|
+
constructor(message: string) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "TimeoutError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
export async function withTimeout<T>(
|
|
19
33
|
ms: number,
|
|
20
34
|
timeoutMessage: string | (() => string),
|
|
@@ -23,7 +37,7 @@ export async function withTimeout<T>(
|
|
|
23
37
|
return new Promise<T>((resolve, reject) => {
|
|
24
38
|
const timer = setTimeout(() => {
|
|
25
39
|
const message = typeof timeoutMessage === "function" ? timeoutMessage() : timeoutMessage;
|
|
26
|
-
reject(new
|
|
40
|
+
reject(new TimeoutError(message));
|
|
27
41
|
}, ms);
|
|
28
42
|
|
|
29
43
|
fn()
|
|
@@ -317,10 +317,50 @@ describe("executeHumanInputTask", () => {
|
|
|
317
317
|
expect(resolved.taskName).toBe("gate");
|
|
318
318
|
expect(resolved.outcome).toBe("reject");
|
|
319
319
|
expect(resolved.resolvedBy).toBe("carol");
|
|
320
|
+
expect(resolved.resolvedByActor).toBeUndefined();
|
|
320
321
|
expect(resolved.autoResolved).toBe(false);
|
|
321
322
|
expect(resolved.waitDurationMs).toBeGreaterThanOrEqual(0);
|
|
322
323
|
});
|
|
323
324
|
|
|
325
|
+
it("carries the reviewer_actor display snapshot into approval_resolved and the task output", async () => {
|
|
326
|
+
const emitted: WorkflowEventDescriptor[][] = [];
|
|
327
|
+
const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
|
|
328
|
+
const reviewerActor = {
|
|
329
|
+
id: "ida_01abc",
|
|
330
|
+
display_name: "Ada Lovelace",
|
|
331
|
+
email: "ada@example.com",
|
|
332
|
+
avatar: "https://example.com/ada.png",
|
|
333
|
+
};
|
|
334
|
+
const awaitFn: AwaitHumanInputFn = async () => ({
|
|
335
|
+
outcome: "approve",
|
|
336
|
+
reviewer: "ida_01abc",
|
|
337
|
+
reviewer_actor: reviewerActor,
|
|
338
|
+
responded_at: "2026-05-20T10:00:00Z",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const taskDef: HumanInputTaskDef = {
|
|
342
|
+
kind: "human_input",
|
|
343
|
+
humanInput: { prompt: "Confirm?" },
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const state = createState();
|
|
347
|
+
await executeHumanInputTask(taskDef, "gate", state, makeCtx(awaitFn, emitFn));
|
|
348
|
+
|
|
349
|
+
const resolved = emitted[1][0];
|
|
350
|
+
if (resolved.type !== "approval_resolved") throw new Error("unexpected");
|
|
351
|
+
expect(resolved.resolvedBy).toBe("ida_01abc");
|
|
352
|
+
expect(resolved.resolvedByActor).toEqual(reviewerActor);
|
|
353
|
+
|
|
354
|
+
// The whole signal payload IS the task output — the actor lands there
|
|
355
|
+
// for downstream tasks (e.g. notification templates) without copying.
|
|
356
|
+
expect(state.data.gate).toEqual({
|
|
357
|
+
outcome: "approve",
|
|
358
|
+
reviewer: "ida_01abc",
|
|
359
|
+
reviewer_actor: reviewerActor,
|
|
360
|
+
responded_at: "2026-05-20T10:00:00Z",
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
324
364
|
it("does not error when emitEvents is undefined (backward compat)", async () => {
|
|
325
365
|
const awaitFn: AwaitHumanInputFn = async () => ({ outcome: "approve" });
|
|
326
366
|
|
|
@@ -109,6 +109,7 @@ export async function executeHumanInputTask(
|
|
|
109
109
|
occurredAt: new Date().toISOString(),
|
|
110
110
|
outcome: result.outcome,
|
|
111
111
|
resolvedBy: result.reviewer ?? "",
|
|
112
|
+
resolvedByActor: result.reviewer_actor,
|
|
112
113
|
comment: "",
|
|
113
114
|
waitDurationMs: Date.now() - approvalRequestedAt,
|
|
114
115
|
autoResolved: result.auto_resolved ?? false,
|
|
@@ -563,13 +563,33 @@ export interface HumanInputExecutionConfig {
|
|
|
563
563
|
|
|
564
564
|
export interface HumanInputResult {
|
|
565
565
|
readonly outcome: string;
|
|
566
|
+
/** Canonical reviewer identity (identity-account ID). Empty/absent when unattributed. */
|
|
566
567
|
readonly reviewer?: string;
|
|
568
|
+
/**
|
|
569
|
+
* Display snapshot of the reviewer, stamped server-side by the control
|
|
570
|
+
* plane at decision time. Rides the signal opaquely into the task output
|
|
571
|
+
* (for downstream tasks, e.g. notifications) and the approval_resolved
|
|
572
|
+
* event (for UIs). Absent when no attribution exists — never synthesized.
|
|
573
|
+
*/
|
|
574
|
+
readonly reviewer_actor?: HumanInputReviewerActor;
|
|
567
575
|
readonly responded_at?: string;
|
|
568
576
|
readonly form_data?: Record<string, unknown>;
|
|
569
577
|
readonly auto_resolved?: boolean;
|
|
570
578
|
readonly reason?: string;
|
|
571
579
|
}
|
|
572
580
|
|
|
581
|
+
/** Write-time snapshot of the reviewer's display identity. */
|
|
582
|
+
export interface HumanInputReviewerActor {
|
|
583
|
+
/** Canonical identity (matches HumanInputResult.reviewer). */
|
|
584
|
+
readonly id: string;
|
|
585
|
+
/** Human-readable name; empty when unknown. */
|
|
586
|
+
readonly display_name?: string;
|
|
587
|
+
/** Email address; empty when unknown. */
|
|
588
|
+
readonly email?: string;
|
|
589
|
+
/** Avatar URL; empty when unknown. */
|
|
590
|
+
readonly avatar?: string;
|
|
591
|
+
}
|
|
592
|
+
|
|
573
593
|
export interface HumanInputConfig {
|
|
574
594
|
readonly prompt: string;
|
|
575
595
|
readonly outcomes?: HumanInputOutcome[];
|
|
@@ -843,7 +863,10 @@ export interface ApprovalRequestedEvent extends EventBase {
|
|
|
843
863
|
export interface ApprovalResolvedEvent extends EventBase {
|
|
844
864
|
readonly type: "approval_resolved";
|
|
845
865
|
readonly outcome: string;
|
|
866
|
+
/** Canonical reviewer identity. Empty when unattributed. */
|
|
846
867
|
readonly resolvedBy: string;
|
|
868
|
+
/** Display snapshot of the reviewer from the signal payload, if stamped. */
|
|
869
|
+
readonly resolvedByActor?: HumanInputReviewerActor;
|
|
847
870
|
readonly comment: string;
|
|
848
871
|
readonly waitDurationMs: number;
|
|
849
872
|
readonly autoResolved: boolean;
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* - "approve": returns auto-approved output
|
|
9
9
|
* - "deny": returns auto-denied output
|
|
10
10
|
*
|
|
11
|
-
* Signal payload shape:
|
|
11
|
+
* Signal payload shape:
|
|
12
|
+
* { outcome, form_data?, reviewer, reviewer_actor?, responded_at }
|
|
12
13
|
*
|
|
13
14
|
* TEMPORAL SANDBOX: This file runs inside the deterministic workflow isolate.
|
|
14
15
|
*/
|