@stigmer/server 3.37.0 → 3.38.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/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +3 -0
- package/dist/boot/compose.js.map +1 -1
- package/dist/domain/agentexecution/artifacts.d.ts.map +1 -1
- package/dist/domain/agentexecution/artifacts.js +15 -6
- package/dist/domain/agentexecution/artifacts.js.map +1 -1
- package/dist/domain/agentexecution/lifecycle.d.ts.map +1 -1
- package/dist/domain/agentexecution/lifecycle.js +5 -3
- package/dist/domain/agentexecution/lifecycle.js.map +1 -1
- package/dist/domain/agentexecution/platform-rows.d.ts +63 -0
- package/dist/domain/agentexecution/platform-rows.d.ts.map +1 -0
- package/dist/domain/agentexecution/platform-rows.js +106 -0
- package/dist/domain/agentexecution/platform-rows.js.map +1 -0
- package/dist/domain/agentexecution/subscribe.d.ts.map +1 -1
- package/dist/domain/agentexecution/subscribe.js +7 -4
- package/dist/domain/agentexecution/subscribe.js.map +1 -1
- package/dist/domain/agentexecution/update-status.d.ts.map +1 -1
- package/dist/domain/agentexecution/update-status.js +48 -17
- package/dist/domain/agentexecution/update-status.js.map +1 -1
- package/dist/domain/iampolicy/grant-path.d.ts +7 -3
- package/dist/domain/iampolicy/grant-path.d.ts.map +1 -1
- package/dist/domain/iampolicy/grant-path.js +10 -2
- package/dist/domain/iampolicy/grant-path.js.map +1 -1
- package/dist/domain/organization/controller.d.ts +4 -1
- package/dist/domain/organization/controller.d.ts.map +1 -1
- package/dist/domain/organization/controller.js +28 -4
- package/dist/domain/organization/controller.js.map +1 -1
- package/dist/domain/organization/steps.d.ts +22 -0
- package/dist/domain/organization/steps.d.ts.map +1 -1
- package/dist/domain/organization/steps.js +50 -2
- package/dist/domain/organization/steps.js.map +1 -1
- package/dist/domain/workflowexecution/create-steps.d.ts.map +1 -1
- package/dist/domain/workflowexecution/create-steps.js +6 -2
- package/dist/domain/workflowexecution/create-steps.js.map +1 -1
- package/dist/domain/workflowexecution/lifecycle.d.ts.map +1 -1
- package/dist/domain/workflowexecution/lifecycle.js +5 -2
- package/dist/domain/workflowexecution/lifecycle.js.map +1 -1
- package/dist/domain/workflowexecution/send-signal.d.ts.map +1 -1
- package/dist/domain/workflowexecution/send-signal.js +6 -3
- package/dist/domain/workflowexecution/send-signal.js.map +1 -1
- package/dist/domain/workflowexecution/submit-approval.d.ts.map +1 -1
- package/dist/domain/workflowexecution/submit-approval.js +6 -2
- package/dist/domain/workflowexecution/submit-approval.js.map +1 -1
- package/dist/domain/workflowexecution/submit-file-decision.d.ts.map +1 -1
- package/dist/domain/workflowexecution/submit-file-decision.js +6 -2
- package/dist/domain/workflowexecution/submit-file-decision.js.map +1 -1
- package/dist/domain/workflowexecution/submit-workflow-task-approval.d.ts.map +1 -1
- package/dist/domain/workflowexecution/submit-workflow-task-approval.js +6 -2
- package/dist/domain/workflowexecution/submit-workflow-task-approval.js.map +1 -1
- package/dist/domain/workflowexecution/subscribe-events.d.ts.map +1 -1
- package/dist/domain/workflowexecution/subscribe-events.js +6 -2
- package/dist/domain/workflowexecution/subscribe-events.js.map +1 -1
- package/dist/domain/workflowexecution/subscribe.d.ts.map +1 -1
- package/dist/domain/workflowexecution/subscribe.js +7 -4
- package/dist/domain/workflowexecution/subscribe.js.map +1 -1
- package/dist/extensions/gate-slots.d.ts +14 -1
- package/dist/extensions/gate-slots.d.ts.map +1 -1
- package/dist/extensions/gate-slots.js +1 -0
- package/dist/extensions/gate-slots.js.map +1 -1
- package/dist/pipeline/steps/authorization-tuples.d.ts +6 -2
- package/dist/pipeline/steps/authorization-tuples.d.ts.map +1 -1
- package/dist/pipeline/steps/authorization-tuples.js +6 -2
- package/dist/pipeline/steps/authorization-tuples.js.map +1 -1
- package/dist/temporal/agentexecution/runner-result.d.ts +12 -0
- package/dist/temporal/agentexecution/runner-result.d.ts.map +1 -1
- package/dist/temporal/agentexecution/runner-result.js +15 -0
- package/dist/temporal/agentexecution/runner-result.js.map +1 -1
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.d.ts.map +1 -1
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js +44 -22
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js.map +1 -1
- package/package.json +6 -6
- package/src/boot/compose.ts +3 -0
- package/src/domain/agentexecution/__tests__/store-faults.test.ts +315 -0
- package/src/domain/agentexecution/__tests__/update-status.test.ts +217 -0
- package/src/domain/agentexecution/artifacts.ts +21 -12
- package/src/domain/agentexecution/lifecycle.ts +5 -3
- package/src/domain/agentexecution/platform-rows.ts +133 -0
- package/src/domain/agentexecution/subscribe.ts +7 -3
- package/src/domain/agentexecution/update-status.ts +70 -19
- package/src/domain/iampolicy/__tests__/grant-path.test.ts +40 -7
- package/src/domain/iampolicy/grant-path.ts +19 -5
- package/src/domain/organization/__tests__/organization-delete.test.ts +266 -0
- package/src/domain/organization/controller.ts +40 -8
- package/src/domain/organization/steps.ts +66 -2
- package/src/domain/workflowexecution/__tests__/store-faults.test.ts +418 -0
- package/src/domain/workflowexecution/create-steps.ts +6 -2
- package/src/domain/workflowexecution/lifecycle.ts +5 -2
- package/src/domain/workflowexecution/send-signal.ts +11 -4
- package/src/domain/workflowexecution/submit-approval.ts +6 -2
- package/src/domain/workflowexecution/submit-file-decision.ts +6 -2
- package/src/domain/workflowexecution/submit-workflow-task-approval.ts +6 -2
- package/src/domain/workflowexecution/subscribe-events.ts +6 -2
- package/src/domain/workflowexecution/subscribe.ts +11 -4
- package/src/extensions/__tests__/registry.test.ts +3 -0
- package/src/extensions/gate-slots.ts +14 -0
- package/src/pipeline/__tests__/blind-not-found.test.ts +297 -0
- package/src/pipeline/steps/authorization-tuples.ts +6 -2
- package/src/temporal/agentexecution/__tests__/invoke-workflow.test.ts +51 -1
- package/src/temporal/agentexecution/__tests__/own-behalf-status-writes.test.ts +149 -9
- package/src/temporal/agentexecution/__tests__/replay-histories/runner-failed.json +757 -0
- package/src/temporal/agentexecution/__tests__/replay-histories/user-cancel.json +755 -0
- package/src/temporal/agentexecution/__tests__/replay.test.ts +4 -2
- package/src/temporal/agentexecution/runner-result.ts +16 -0
- package/src/temporal/agentexecution/workflows/invoke-agent-execution.ts +49 -22
- package/src/domain/agentexecution/__tests__/execution-usage-report.test.ts +0 -78
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds the error contract's store-fault rule structurally: no error handler
|
|
3
|
+
* in the server answers NotFound without looking at the error it caught.
|
|
4
|
+
* Only a typed not-found (`ResourceNotFoundError` and its kin) means the
|
|
5
|
+
* record is missing; any other failure is an infrastructure fault answered
|
|
6
|
+
* through `internalError` (`src/pipeline/errors.ts`), because a client told
|
|
7
|
+
* "not found" may discard state that still exists
|
|
8
|
+
* (`.agents/skills/ts-server-dev-guidelines/SKILL.md`, "Errors").
|
|
9
|
+
*
|
|
10
|
+
* A *blind NotFound* is a `throw` of a NotFound that is a direct statement of
|
|
11
|
+
* a `catch` block or of a `.catch(...)` callback — not nested under any
|
|
12
|
+
* condition. That is the defect's exact shape: a discriminating handler throws
|
|
13
|
+
* its NotFound under `if (error instanceof ...)` or a code check, so it passes
|
|
14
|
+
* whatever error class it tests, and the guard needs no list of them. A throw
|
|
15
|
+
* constructs a NotFound when it is `new ConnectError(..., Code.NotFound)` or
|
|
16
|
+
* a call to a NotFound constructor; the constructors are derived from the
|
|
17
|
+
* source (`notFoundError`, plus every function whose body is a single
|
|
18
|
+
* `return` of a NotFound construction, to a fixed point), so a domain's own
|
|
19
|
+
* wrapper cannot hide a blind site, and a discriminating mapper is not one.
|
|
20
|
+
*
|
|
21
|
+
* The scan parses each module with the TypeScript compiler API rather than a
|
|
22
|
+
* regular expression, because a catch block's extent (nested braces, a log
|
|
23
|
+
* line before the throw) is a syntax question. `PENDING` is the list of known
|
|
24
|
+
* blind sites still to be fixed; the scan must equal it exactly, so a new
|
|
25
|
+
* blind site fails here, and so does a fixed site still listed.
|
|
26
|
+
*/
|
|
27
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
|
|
31
|
+
import ts from "typescript";
|
|
32
|
+
import { describe, expect, it } from "vitest";
|
|
33
|
+
|
|
34
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const SRC = path.resolve(HERE, "../..");
|
|
36
|
+
|
|
37
|
+
/** Known blind sites by module (relative to `src`), with their count. */
|
|
38
|
+
const PENDING: ReadonlyMap<string, number> = new Map([
|
|
39
|
+
// Store loads, stigmer/stigmer#1345 (fixed domain by domain).
|
|
40
|
+
["domain/agent/controller.ts", 1],
|
|
41
|
+
["domain/agentchannel/controller.ts", 1],
|
|
42
|
+
["domain/agentinstance/controller.ts", 1],
|
|
43
|
+
["domain/agentshare/controller.ts", 1],
|
|
44
|
+
["domain/artifact/controller.ts", 1],
|
|
45
|
+
["domain/environment/controller.ts", 1],
|
|
46
|
+
["domain/environment/steps.ts", 1],
|
|
47
|
+
["domain/mcpserver/complete-oauth-connect.ts", 1],
|
|
48
|
+
["domain/mcpserver/connect.ts", 1],
|
|
49
|
+
["domain/mcpserver/controller.ts", 1],
|
|
50
|
+
["domain/mcpserver/initiate-oauth-connect.ts", 1],
|
|
51
|
+
["domain/mcpserver/start-connect.ts", 2],
|
|
52
|
+
["domain/plugin/controller.ts", 1],
|
|
53
|
+
["domain/skill/controller.ts", 1],
|
|
54
|
+
["domain/workflow/controller.ts", 1],
|
|
55
|
+
["domain/workflowinstance/controller.ts", 1],
|
|
56
|
+
// Parent loads through the in-process chain that fold every RPC error,
|
|
57
|
+
// validation included, stigmer/stigmer#1351.
|
|
58
|
+
["domain/agentinstance/steps.ts", 1],
|
|
59
|
+
["domain/workflowinstance/steps.ts", 1],
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
interface BlindSite {
|
|
63
|
+
readonly module: string;
|
|
64
|
+
readonly line: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isCodeNotFound(node: ts.Expression): boolean {
|
|
68
|
+
return (
|
|
69
|
+
ts.isPropertyAccessExpression(node) &&
|
|
70
|
+
ts.isIdentifier(node.expression) &&
|
|
71
|
+
node.expression.text === "Code" &&
|
|
72
|
+
node.name.text === "NotFound"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isNotFoundConstruction(
|
|
77
|
+
node: ts.Expression | undefined,
|
|
78
|
+
constructors: ReadonlySet<string>,
|
|
79
|
+
): boolean {
|
|
80
|
+
if (node === undefined) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (ts.isCallExpression(node)) {
|
|
84
|
+
return (
|
|
85
|
+
ts.isIdentifier(node.expression) && constructors.has(node.expression.text)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (ts.isNewExpression(node)) {
|
|
89
|
+
return (
|
|
90
|
+
ts.isIdentifier(node.expression) &&
|
|
91
|
+
node.expression.text === "ConnectError" &&
|
|
92
|
+
(node.arguments ?? []).some(isCodeNotFound)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function forEachNode(node: ts.Node, visit: (node: ts.Node) => void): void {
|
|
99
|
+
visit(node);
|
|
100
|
+
ts.forEachChild(node, (child) => forEachNode(child, visit));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** `notFoundError` plus every single-`return` wrapper of a NotFound. */
|
|
104
|
+
function notFoundConstructors(
|
|
105
|
+
sources: readonly ts.SourceFile[],
|
|
106
|
+
): ReadonlySet<string> {
|
|
107
|
+
const constructors = new Set(["notFoundError"]);
|
|
108
|
+
let grew = true;
|
|
109
|
+
while (grew) {
|
|
110
|
+
grew = false;
|
|
111
|
+
for (const source of sources) {
|
|
112
|
+
forEachNode(source, (node) => {
|
|
113
|
+
if (
|
|
114
|
+
!ts.isFunctionDeclaration(node) ||
|
|
115
|
+
node.name === undefined ||
|
|
116
|
+
node.body === undefined ||
|
|
117
|
+
node.body.statements.length !== 1 ||
|
|
118
|
+
constructors.has(node.name.text)
|
|
119
|
+
) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const only = node.body.statements[0];
|
|
123
|
+
if (
|
|
124
|
+
only !== undefined &&
|
|
125
|
+
ts.isReturnStatement(only) &&
|
|
126
|
+
isNotFoundConstruction(only.expression, constructors)
|
|
127
|
+
) {
|
|
128
|
+
constructors.add(node.name.text);
|
|
129
|
+
grew = true;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return constructors;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The handler block of a `.catch(fn)` call, when `fn` has a block body. */
|
|
138
|
+
function catchCallbackBlock(node: ts.Node): ts.Block | undefined {
|
|
139
|
+
if (
|
|
140
|
+
!ts.isCallExpression(node) ||
|
|
141
|
+
!ts.isPropertyAccessExpression(node.expression) ||
|
|
142
|
+
node.expression.name.text !== "catch"
|
|
143
|
+
) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const handler = node.arguments[0];
|
|
147
|
+
if (
|
|
148
|
+
handler !== undefined &&
|
|
149
|
+
(ts.isArrowFunction(handler) || ts.isFunctionExpression(handler)) &&
|
|
150
|
+
ts.isBlock(handler.body)
|
|
151
|
+
) {
|
|
152
|
+
return handler.body;
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function blindSites(
|
|
158
|
+
source: ts.SourceFile,
|
|
159
|
+
module: string,
|
|
160
|
+
constructors: ReadonlySet<string>,
|
|
161
|
+
): BlindSite[] {
|
|
162
|
+
const sites: BlindSite[] = [];
|
|
163
|
+
forEachNode(source, (node) => {
|
|
164
|
+
const block = ts.isCatchClause(node)
|
|
165
|
+
? node.block
|
|
166
|
+
: catchCallbackBlock(node);
|
|
167
|
+
if (block === undefined) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
for (const statement of block.statements) {
|
|
171
|
+
if (
|
|
172
|
+
ts.isThrowStatement(statement) &&
|
|
173
|
+
isNotFoundConstruction(statement.expression, constructors)
|
|
174
|
+
) {
|
|
175
|
+
const { line } = source.getLineAndCharacterOfPosition(
|
|
176
|
+
statement.getStart(source),
|
|
177
|
+
);
|
|
178
|
+
sites.push({ module, line: line + 1 });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
return sites;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parse(fileName: string, text: string): ts.SourceFile {
|
|
186
|
+
return ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Every non-test module under `src`, parsed. */
|
|
190
|
+
function serverSources(): ts.SourceFile[] {
|
|
191
|
+
const sources: ts.SourceFile[] = [];
|
|
192
|
+
const walk = (dir: string): void => {
|
|
193
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
194
|
+
const full = path.join(dir, entry.name);
|
|
195
|
+
if (entry.isDirectory()) {
|
|
196
|
+
if (entry.name !== "__tests__") {
|
|
197
|
+
walk(full);
|
|
198
|
+
}
|
|
199
|
+
} else if (
|
|
200
|
+
entry.name.endsWith(".ts") &&
|
|
201
|
+
!entry.name.endsWith(".d.ts") &&
|
|
202
|
+
!entry.name.endsWith(".test.ts")
|
|
203
|
+
) {
|
|
204
|
+
sources.push(parse(full, readFileSync(full, "utf8")));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
walk(SRC);
|
|
209
|
+
return sources;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The rule applied to one inline module, for the rule's own cases. */
|
|
213
|
+
function blindLinesOf(text: string): number[] {
|
|
214
|
+
const source = parse("fixture.ts", text);
|
|
215
|
+
return blindSites(source, "fixture.ts", notFoundConstructors([source])).map(
|
|
216
|
+
(site) => site.line,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
describe("the blind-NotFound rule", () => {
|
|
221
|
+
it("flags an unconditional NotFound in a catch block, bound or not", () => {
|
|
222
|
+
expect(
|
|
223
|
+
blindLinesOf(`
|
|
224
|
+
try { load(); } catch { throw notFoundError("agent", id); }
|
|
225
|
+
try { load(); } catch (error) {
|
|
226
|
+
log(error);
|
|
227
|
+
throw new ConnectError(copy(id), Code.NotFound);
|
|
228
|
+
}
|
|
229
|
+
`),
|
|
230
|
+
).toEqual([2, 5]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("flags a NotFound built by a derived wrapper and one in a .catch callback", () => {
|
|
234
|
+
expect(
|
|
235
|
+
blindLinesOf(`
|
|
236
|
+
function missing(id: string) { return notFoundError("agent", id); }
|
|
237
|
+
function missingAgain(id: string) { return missing(id); }
|
|
238
|
+
try { load(); } catch { throw missingAgain(id); }
|
|
239
|
+
load().catch(() => { throw missing(id); });
|
|
240
|
+
`),
|
|
241
|
+
).toEqual([4, 5]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("passes a NotFound thrown only under a test of the error", () => {
|
|
245
|
+
expect(
|
|
246
|
+
blindLinesOf(`
|
|
247
|
+
try { load(); } catch (error) {
|
|
248
|
+
if (error instanceof ResourceNotFoundError) {
|
|
249
|
+
throw notFoundError("agent", id);
|
|
250
|
+
}
|
|
251
|
+
throw internalError(error, "failed to load agent");
|
|
252
|
+
}
|
|
253
|
+
`),
|
|
254
|
+
).toEqual([]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("does not treat a discriminating mapper as a constructor", () => {
|
|
258
|
+
expect(
|
|
259
|
+
blindLinesOf(`
|
|
260
|
+
function mapNotFound(error: unknown, id: string): unknown {
|
|
261
|
+
if (error instanceof WorkflowNotFoundError) {
|
|
262
|
+
return notFoundError("workflow", id);
|
|
263
|
+
}
|
|
264
|
+
return error;
|
|
265
|
+
}
|
|
266
|
+
try { load(); } catch (error) { throw mapNotFound(error, id); }
|
|
267
|
+
`),
|
|
268
|
+
).toEqual([]);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("the server source", () => {
|
|
273
|
+
it("has no blind NotFound beyond the pending list, and the list names only live sites", () => {
|
|
274
|
+
const sources = serverSources();
|
|
275
|
+
const constructors = notFoundConstructors(sources);
|
|
276
|
+
const found = new Map<string, number[]>();
|
|
277
|
+
for (const source of sources) {
|
|
278
|
+
const module = path
|
|
279
|
+
.relative(SRC, source.fileName)
|
|
280
|
+
.split(path.sep)
|
|
281
|
+
.join("/");
|
|
282
|
+
for (const site of blindSites(source, module, constructors)) {
|
|
283
|
+
found.set(module, [...(found.get(module) ?? []), site.line]);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const counts = Object.fromEntries(
|
|
288
|
+
[...found].map(([module, lines]) => [module, lines.length]),
|
|
289
|
+
);
|
|
290
|
+
const where = [...found]
|
|
291
|
+
.flatMap(([module, lines]) => lines.map((line) => `${module}:${line}`))
|
|
292
|
+
.join("\n");
|
|
293
|
+
expect(counts, `blind NotFound sites found:\n${where}`).toEqual(
|
|
294
|
+
Object.fromEntries(PENDING),
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -330,8 +330,12 @@ export function newCreateAuthorizationTuplesStep<Desc extends DescMessage>(
|
|
|
330
330
|
/**
|
|
331
331
|
* CleanupIamPolicies — after the store delete in every delete chain.
|
|
332
332
|
* Best-effort by contract: a driver failure is logged and the delete
|
|
333
|
-
* succeeds (Java's DeleteOperationCleanupIamPoliciesStep swallows all
|
|
334
|
-
*
|
|
333
|
+
* succeeds (Java's DeleteOperationCleanupIamPoliciesStep swallows all),
|
|
334
|
+
* because a deleted resource's rows grant nothing once it is gone. The
|
|
335
|
+
* organization is the one kind whose id another caller can take next, so
|
|
336
|
+
* its chain revokes the organization's rows before the row, failing
|
|
337
|
+
* closed (RevokeOrganizationPolicies, domain/organization/steps.ts), and
|
|
338
|
+
* this step is only its backstop there.
|
|
335
339
|
*/
|
|
336
340
|
export function newCleanupIamPoliciesStep<Desc extends DescMessage>(
|
|
337
341
|
lifecycle: ResourceAuthorizationLifecycle | undefined,
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* - bounded auto-recovery on worker-shutdown shapes (#776: IN_PROGRESS
|
|
17
17
|
* persisted, honest status copy on exhaustion);
|
|
18
18
|
* - EXECUTION_FAILED propagation with the fallback persist;
|
|
19
|
+
* - the stop and failure copy each has one writer (stigmer#980): the
|
|
20
|
+
* pause persist carries the pause row, a runner-reported failure's
|
|
21
|
+
* write carries no lines (the runner explained it), and only a broken
|
|
22
|
+
* flow gets the platform's two lines;
|
|
19
23
|
* - external cancellation cleanup (CANCELLED persisted quietly —
|
|
20
24
|
* stigmer#282 — and the EC deleted);
|
|
21
25
|
* - callback-token completion on success AND failure (the DD-001 lane,
|
|
@@ -50,9 +54,15 @@ import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/ag
|
|
|
50
54
|
import {
|
|
51
55
|
ExecutionPhase,
|
|
52
56
|
FileChangeSetStatus,
|
|
57
|
+
MessageType,
|
|
53
58
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
54
59
|
import { Harness } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
55
60
|
|
|
61
|
+
import {
|
|
62
|
+
CANCEL_STOP_ROW,
|
|
63
|
+
PAUSE_STOP_ROW,
|
|
64
|
+
} from "../../../domain/agentexecution/platform-rows.js";
|
|
65
|
+
|
|
56
66
|
import {
|
|
57
67
|
INVOKE_AGENT_EXECUTION_WORKFLOW_NAME,
|
|
58
68
|
MEMO_ACTIVITY_TASK_QUEUE,
|
|
@@ -565,6 +575,12 @@ describe("invoke-agent-execution workflow (TestWorkflowEnvironment)", () => {
|
|
|
565
575
|
.includes(ExecutionPhase.EXECUTION_IN_PROGRESS),
|
|
566
576
|
"resume re-asserts IN_PROGRESS after the PAUSED persist",
|
|
567
577
|
).toBe(true);
|
|
578
|
+
// The workflow is the pause row's one writer: the runner cannot tell
|
|
579
|
+
// this stop from a cancel, so it settles without a row.
|
|
580
|
+
expect(
|
|
581
|
+
script.persistedStatuses[pausedAt]!.status.messages.map((m) => [m.type, m.content]),
|
|
582
|
+
"the PAUSED persist carries the pause row",
|
|
583
|
+
).toEqual([[MessageType.MESSAGE_SYSTEM, PAUSE_STOP_ROW]]);
|
|
568
584
|
}, 60_000);
|
|
569
585
|
|
|
570
586
|
it("auto-recovers a worker-shutdown interruption and persists IN_PROGRESS (#776)", async (testCtx) => { if (!envReady) return testCtx.skip();
|
|
@@ -611,6 +627,40 @@ describe("invoke-agent-execution workflow (TestWorkflowEnvironment)", () => {
|
|
|
611
627
|
);
|
|
612
628
|
expect(failed.length).toBeGreaterThan(0);
|
|
613
629
|
expect(failed[0]!.status.error).toBe("agent blew up");
|
|
630
|
+
// The runner already explained its own failure in the transcript, so
|
|
631
|
+
// no FAILED write of the workflow adds a second explanation.
|
|
632
|
+
expect(
|
|
633
|
+
failed.flatMap((entry) => entry.status.messages),
|
|
634
|
+
"a runner-reported failure adds no platform lines",
|
|
635
|
+
).toEqual([]);
|
|
636
|
+
expectExecutionContextDeleted();
|
|
637
|
+
}, 30_000);
|
|
638
|
+
|
|
639
|
+
it("explains a flow that broke without a runner verdict with the platform's two lines", async (testCtx) => { if (!envReady) return testCtx.skip();
|
|
640
|
+
const { ApplicationFailure } = await import("@temporalio/common");
|
|
641
|
+
script.executeBehaviors = [
|
|
642
|
+
async () => {
|
|
643
|
+
// Not recoverable (not a cancellation, heartbeat timeout or worker
|
|
644
|
+
// shutdown), so the flow breaks with no FAILED result to read.
|
|
645
|
+
throw ApplicationFailure.create({ message: "runner crashed", nonRetryable: true });
|
|
646
|
+
},
|
|
647
|
+
];
|
|
648
|
+
|
|
649
|
+
const handle = await startWorkflow(workflowInput());
|
|
650
|
+
await expect(handle.result()).rejects.toThrow(/Workflow execution failed/);
|
|
651
|
+
|
|
652
|
+
const failed = script.persistedStatuses.filter(
|
|
653
|
+
(entry) => entry.status.phase === ExecutionPhase.EXECUTION_FAILED,
|
|
654
|
+
);
|
|
655
|
+
expect(failed.length).toBeGreaterThan(0);
|
|
656
|
+
const lines = failed.at(-1)!.status.messages;
|
|
657
|
+
expect(lines.map((m) => m.type)).toEqual([
|
|
658
|
+
MessageType.MESSAGE_SYSTEM,
|
|
659
|
+
MessageType.MESSAGE_SYSTEM,
|
|
660
|
+
]);
|
|
661
|
+
// The details carry the wrapped activity error (wrapActivityError's
|
|
662
|
+
// operator-actionable prefix), not a runner-authored sentence.
|
|
663
|
+
expect(lines[1]!.content).toMatch(/^Error details: activity 'ExecuteDeepAgent'/);
|
|
614
664
|
expectExecutionContextDeleted();
|
|
615
665
|
}, 30_000);
|
|
616
666
|
|
|
@@ -638,7 +688,7 @@ describe("invoke-agent-execution workflow (TestWorkflowEnvironment)", () => {
|
|
|
638
688
|
// system message is the durable marker.
|
|
639
689
|
expect(cancelled.status.error).toBe("");
|
|
640
690
|
expect(cancelled.status.messages.map((message) => message.content)).toEqual([
|
|
641
|
-
|
|
691
|
+
CANCEL_STOP_ROW,
|
|
642
692
|
]);
|
|
643
693
|
expectExecutionContextDeleted();
|
|
644
694
|
}, 60_000);
|
|
@@ -20,19 +20,24 @@
|
|
|
20
20
|
* execution status` from authorize.ts — the dark production composition's
|
|
21
21
|
* exact log line.
|
|
22
22
|
*
|
|
23
|
-
* The
|
|
23
|
+
* The payload shapes are the workflow's real ones
|
|
24
24
|
* (invoke-agent-execution.ts): updateStatusOnFailure's FAILED with two
|
|
25
|
-
* MESSAGE_SYSTEM lines
|
|
25
|
+
* MESSAGE_SYSTEM lines (a flow that broke) or none (a runner-reported
|
|
26
|
+
* failure), updateStatusOnCancellation's CANCELLED with its stop row,
|
|
26
27
|
* and the phase-only persistFinalStatus / persistInterruptedStatus /
|
|
27
|
-
* persistResumedStatus shape.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* persistResumedStatus shape.
|
|
29
|
+
*
|
|
30
|
+
* The last arms pin the transcript a stopped execution keeps over this
|
|
31
|
+
* production lane (stigmer#980): the platform's lines are appended to the
|
|
32
|
+
* runner's transcript, never substituted for it, and the cancel row stays
|
|
33
|
+
* last when the runner's settle write lands after it. The merge's full
|
|
34
|
+
* case matrix is domain/agentexecution/__tests__/update-status.test.ts.
|
|
30
35
|
*/
|
|
31
36
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
32
37
|
import { tmpdir } from "node:os";
|
|
33
38
|
import path from "node:path";
|
|
34
39
|
import { create, toJson } from "@bufbuild/protobuf";
|
|
35
|
-
import type { JsonValue } from "@bufbuild/protobuf";
|
|
40
|
+
import type { JsonValue, MessageInitShape } from "@bufbuild/protobuf";
|
|
36
41
|
import { createClient } from "@connectrpc/connect";
|
|
37
42
|
import type { Client } from "@temporalio/client";
|
|
38
43
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
@@ -46,9 +51,12 @@ import {
|
|
|
46
51
|
ExecutionPhase,
|
|
47
52
|
MessageType,
|
|
48
53
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
54
|
+
import { AgentExecutionUpdateStatusInputSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/io_pb";
|
|
55
|
+
import type { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
49
56
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
50
57
|
|
|
51
58
|
import { loadConfig } from "../../../boot/config.js";
|
|
59
|
+
import { CANCEL_STOP_ROW } from "../../../domain/agentexecution/platform-rows.js";
|
|
52
60
|
import { composeServer } from "../../../boot/compose.js";
|
|
53
61
|
import type { ComposedServer } from "../../../boot/compose.js";
|
|
54
62
|
import { createLogger } from "../../../boot/logger.js";
|
|
@@ -147,7 +155,11 @@ describe("own-behalf status writes under an enforcing Authorizer", () => {
|
|
|
147
155
|
transitions.length = 0;
|
|
148
156
|
});
|
|
149
157
|
|
|
150
|
-
async function seedExecution(
|
|
158
|
+
async function seedExecution(
|
|
159
|
+
id: string,
|
|
160
|
+
phase: ExecutionPhase = ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
161
|
+
messages: MessageInitShape<typeof AgentMessageSchema>[] = [],
|
|
162
|
+
): Promise<void> {
|
|
151
163
|
// Seeded through the store: the create path is not under test, and
|
|
152
164
|
// the denying Authorizer would refuse it over the wire.
|
|
153
165
|
await server.store.saveResource(
|
|
@@ -159,7 +171,7 @@ describe("own-behalf status writes under an enforcing Authorizer", () => {
|
|
|
159
171
|
kind: "AgentExecution",
|
|
160
172
|
metadata: { id, name: "own-behalf", org: "acme" },
|
|
161
173
|
spec: { agentId: "agt_1", sessionId: "ses_1" },
|
|
162
|
-
status: { phase
|
|
174
|
+
status: { phase, messages },
|
|
163
175
|
}),
|
|
164
176
|
);
|
|
165
177
|
}
|
|
@@ -253,7 +265,7 @@ describe("own-behalf status writes under an enforcing Authorizer", () => {
|
|
|
253
265
|
messages: [
|
|
254
266
|
{
|
|
255
267
|
type: MessageType.MESSAGE_SYSTEM,
|
|
256
|
-
content:
|
|
268
|
+
content: CANCEL_STOP_ROW,
|
|
257
269
|
},
|
|
258
270
|
],
|
|
259
271
|
}),
|
|
@@ -296,6 +308,134 @@ describe("own-behalf status writes under an enforcing Authorizer", () => {
|
|
|
296
308
|
expect(authorizerCalls).toHaveLength(0);
|
|
297
309
|
});
|
|
298
310
|
|
|
311
|
+
describe("the transcript a stopped execution keeps (stigmer#980)", () => {
|
|
312
|
+
const runnerTurns = [
|
|
313
|
+
{ type: MessageType.MESSAGE_AI, content: "a1" },
|
|
314
|
+
{ type: MessageType.MESSAGE_AI, content: "a2" },
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
async function transcriptOf(id: string): Promise<string[]> {
|
|
318
|
+
return ((await persisted(id)).status?.messages ?? []).map(
|
|
319
|
+
(m) => m.content,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
it("appends the cancel row to a transcript the Cancel RPC already stamped CANCELLED, and keeps it last across the runner's late settle write", async () => {
|
|
324
|
+
await seedExecution(
|
|
325
|
+
"aex_ownbehalf_cancel_race",
|
|
326
|
+
ExecutionPhase.EXECUTION_CANCELLED,
|
|
327
|
+
runnerTurns,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
await updateStatus(
|
|
331
|
+
"aex_ownbehalf_cancel_race",
|
|
332
|
+
toJson(
|
|
333
|
+
AgentExecutionStatusSchema,
|
|
334
|
+
create(AgentExecutionStatusSchema, {
|
|
335
|
+
phase: ExecutionPhase.EXECUTION_CANCELLED,
|
|
336
|
+
messages: [
|
|
337
|
+
{ type: MessageType.MESSAGE_SYSTEM, content: CANCEL_STOP_ROW },
|
|
338
|
+
],
|
|
339
|
+
}),
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
expect(await transcriptOf("aex_ownbehalf_cancel_race")).toEqual([
|
|
343
|
+
"a1",
|
|
344
|
+
"a2",
|
|
345
|
+
CANCEL_STOP_ROW,
|
|
346
|
+
]);
|
|
347
|
+
|
|
348
|
+
// The runner learns of the cancel on its next heartbeat and settles
|
|
349
|
+
// its transcript over the same handler, with no row and no phase.
|
|
350
|
+
await statusWriter.updateStatus(
|
|
351
|
+
create(AgentExecutionUpdateStatusInputSchema, {
|
|
352
|
+
executionId: "aex_ownbehalf_cancel_race",
|
|
353
|
+
status: {
|
|
354
|
+
messages: [
|
|
355
|
+
...runnerTurns,
|
|
356
|
+
{ type: MessageType.MESSAGE_AI, content: "a3" },
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
}),
|
|
360
|
+
);
|
|
361
|
+
expect(await transcriptOf("aex_ownbehalf_cancel_race")).toEqual([
|
|
362
|
+
"a1",
|
|
363
|
+
"a2",
|
|
364
|
+
"a3",
|
|
365
|
+
CANCEL_STOP_ROW,
|
|
366
|
+
]);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("appends a broken flow's two lines to the runner's transcript", async () => {
|
|
370
|
+
await seedExecution(
|
|
371
|
+
"aex_ownbehalf_broken_flow",
|
|
372
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
373
|
+
runnerTurns,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
await updateStatus(
|
|
377
|
+
"aex_ownbehalf_broken_flow",
|
|
378
|
+
toJson(
|
|
379
|
+
AgentExecutionStatusSchema,
|
|
380
|
+
create(AgentExecutionStatusSchema, {
|
|
381
|
+
phase: ExecutionPhase.EXECUTION_FAILED,
|
|
382
|
+
error: "activity crashed",
|
|
383
|
+
messages: [
|
|
384
|
+
{
|
|
385
|
+
type: MessageType.MESSAGE_SYSTEM,
|
|
386
|
+
content: "Internal system error occurred during execution.",
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
type: MessageType.MESSAGE_SYSTEM,
|
|
390
|
+
content: "Error details: activity crashed",
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
}),
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const execution = await persisted("aex_ownbehalf_broken_flow");
|
|
398
|
+
expect(execution.status?.phase).toBe(ExecutionPhase.EXECUTION_FAILED);
|
|
399
|
+
expect(await transcriptOf("aex_ownbehalf_broken_flow")).toEqual([
|
|
400
|
+
"a1",
|
|
401
|
+
"a2",
|
|
402
|
+
"Internal system error occurred during execution.",
|
|
403
|
+
"Error details: activity crashed",
|
|
404
|
+
]);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("leaves a runner-reported failure's transcript as the runner wrote it", async () => {
|
|
408
|
+
await seedExecution(
|
|
409
|
+
"aex_ownbehalf_runner_failed",
|
|
410
|
+
ExecutionPhase.EXECUTION_FAILED,
|
|
411
|
+
[
|
|
412
|
+
...runnerTurns,
|
|
413
|
+
{
|
|
414
|
+
type: MessageType.MESSAGE_SYSTEM,
|
|
415
|
+
content: "Execution failed: the tool refused",
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
await updateStatus(
|
|
421
|
+
"aex_ownbehalf_runner_failed",
|
|
422
|
+
toJson(
|
|
423
|
+
AgentExecutionStatusSchema,
|
|
424
|
+
create(AgentExecutionStatusSchema, {
|
|
425
|
+
phase: ExecutionPhase.EXECUTION_FAILED,
|
|
426
|
+
error: "agent execution failed: the tool refused",
|
|
427
|
+
}),
|
|
428
|
+
),
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
expect(await transcriptOf("aex_ownbehalf_runner_failed")).toEqual([
|
|
432
|
+
"a1",
|
|
433
|
+
"a2",
|
|
434
|
+
"Execution failed: the tool refused",
|
|
435
|
+
]);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
299
439
|
it("surfaces the lane's NotFound as the activity's failure for a deleted execution", async () => {
|
|
300
440
|
await expect(
|
|
301
441
|
updateStatus("aex_ownbehalf_missing", { phase: "EXECUTION_FAILED" }),
|