@jangts/agent-core 0.2.5 → 0.3.6

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 (111) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +386 -50
  3. package/development/docs/api/README.en.md +4 -0
  4. package/development/docs/api/README.md +4 -0
  5. package/development/docs/api/decorators.en.md +83 -0
  6. package/development/docs/api/decorators.md +108 -0
  7. package/development/docs/api/observability.en.md +37 -11
  8. package/development/docs/api/observability.md +64 -11
  9. package/development/docs/api/reference.en.md +469 -348
  10. package/development/docs/api/reference.md +225 -137
  11. package/development/docs/api/types.en.md +1 -0
  12. package/development/docs/api/types.md +1 -0
  13. package/development/docs/api/workflow-views.en.md +64 -0
  14. package/development/docs/api/workflow-views.md +78 -0
  15. package/development/docs/api/workflow.en.md +6 -2
  16. package/development/docs/api/workflow.md +6 -2
  17. package/development/skills/agent-core-app/SKILL.md +16 -0
  18. package/dist/cjs/capabilities/invocation.js +21 -1
  19. package/dist/cjs/capabilities/tool-runtime.js +8 -1
  20. package/dist/cjs/capabilities/tools.js +12 -17
  21. package/dist/cjs/diagnostics/index.js +51 -3
  22. package/dist/cjs/diagnostics/native-sub-flow.js +106 -0
  23. package/dist/cjs/diagnostics/sub-flow.js +278 -32
  24. package/dist/cjs/execution/engine.js +3 -2
  25. package/dist/cjs/execution/events.js +5 -5
  26. package/dist/cjs/execution/reporting.js +18 -28
  27. package/dist/cjs/execution/workflow-chain.js +211 -0
  28. package/dist/cjs/execution/workflow-graph.js +72 -0
  29. package/dist/cjs/execution/workflow.js +34 -6
  30. package/dist/cjs/kits/aggregation.js +17 -14
  31. package/dist/cjs/kits/answering/checks.js +2 -1
  32. package/dist/cjs/kits/answering/flow.js +24 -21
  33. package/dist/cjs/kits/answering/tasks.js +25 -20
  34. package/dist/cjs/kits/delivery.js +36 -24
  35. package/dist/cjs/kits/knowledge/catalog.js +33 -30
  36. package/dist/cjs/kits/policies.js +3 -2
  37. package/dist/cjs/kits/presentation/outputs.js +2 -1
  38. package/dist/cjs/kits/retrieval.js +7 -1
  39. package/dist/cjs/kits/tools.js +1 -0
  40. package/dist/cjs/model/decision-retry.js +2 -1
  41. package/dist/cjs/model/runner.js +20 -5
  42. package/dist/cjs/structs/execution/workflow-views.js +2 -0
  43. package/dist/cjs/support/context/nodes.js +48 -45
  44. package/dist/cjs/support/context/processing.js +6 -3
  45. package/dist/cjs/support/decorators/definition.js +42 -0
  46. package/dist/cjs/support/decorators/index.js +63 -0
  47. package/dist/cjs/support/decorators/pool.js +87 -0
  48. package/dist/cjs/support/decorators/types.js +2 -0
  49. package/dist/cjs/utils/invocation.js +1 -0
  50. package/dist/esm/capabilities/invocation.js +21 -1
  51. package/dist/esm/capabilities/tool-runtime.js +8 -1
  52. package/dist/esm/capabilities/tools.js +12 -17
  53. package/dist/esm/diagnostics/index.d.ts +1 -0
  54. package/dist/esm/diagnostics/index.js +48 -2
  55. package/dist/esm/diagnostics/native-sub-flow.d.ts +94 -0
  56. package/dist/esm/diagnostics/native-sub-flow.js +101 -0
  57. package/dist/esm/diagnostics/sub-flow.d.ts +56 -4
  58. package/dist/esm/diagnostics/sub-flow.js +271 -32
  59. package/dist/esm/execution/engine.d.ts +1 -0
  60. package/dist/esm/execution/engine.js +3 -2
  61. package/dist/esm/execution/events.js +5 -5
  62. package/dist/esm/execution/reporting.d.ts +6 -12
  63. package/dist/esm/execution/reporting.js +19 -29
  64. package/dist/esm/execution/workflow-chain.d.ts +3 -0
  65. package/dist/esm/execution/workflow-chain.js +208 -0
  66. package/dist/esm/execution/workflow-graph.d.ts +7 -0
  67. package/dist/esm/execution/workflow-graph.js +68 -0
  68. package/dist/esm/execution/workflow.d.ts +6 -0
  69. package/dist/esm/execution/workflow.js +34 -6
  70. package/dist/esm/kits/aggregation.d.ts +1 -1
  71. package/dist/esm/kits/aggregation.js +17 -14
  72. package/dist/esm/kits/answering/checks.js +2 -1
  73. package/dist/esm/kits/answering/flow.js +24 -21
  74. package/dist/esm/kits/answering/tasks.js +25 -20
  75. package/dist/esm/kits/delivery.d.ts +0 -9
  76. package/dist/esm/kits/delivery.js +36 -24
  77. package/dist/esm/kits/knowledge/catalog.js +33 -30
  78. package/dist/esm/kits/policies.d.ts +1 -1
  79. package/dist/esm/kits/policies.js +3 -2
  80. package/dist/esm/kits/presentation/outputs.js +2 -1
  81. package/dist/esm/kits/retrieval.js +7 -1
  82. package/dist/esm/kits/tools.js +1 -0
  83. package/dist/esm/model/decision-retry.d.ts +2 -0
  84. package/dist/esm/model/decision-retry.js +2 -1
  85. package/dist/esm/model/runner.d.ts +2 -0
  86. package/dist/esm/model/runner.js +20 -5
  87. package/dist/esm/structs/capabilities/invocation.d.ts +2 -0
  88. package/dist/esm/structs/capabilities/tools.d.ts +7 -3
  89. package/dist/esm/structs/common.d.ts +2 -0
  90. package/dist/esm/structs/debug.d.ts +36 -20
  91. package/dist/esm/structs/execution/workflow-views.d.ts +123 -0
  92. package/dist/esm/structs/execution/workflow-views.js +1 -0
  93. package/dist/esm/structs/execution/workflow.d.ts +4 -3
  94. package/dist/esm/structs/index.d.ts +1 -0
  95. package/dist/esm/structs/model/runtime.d.ts +2 -0
  96. package/dist/esm/support/context/nodes.js +48 -45
  97. package/dist/esm/support/context/processing.js +6 -3
  98. package/dist/esm/support/decorators/definition.d.ts +19 -0
  99. package/dist/esm/support/decorators/definition.js +38 -0
  100. package/dist/esm/support/decorators/index.d.ts +39 -0
  101. package/dist/esm/support/decorators/index.js +48 -0
  102. package/dist/esm/support/decorators/pool.d.ts +97 -0
  103. package/dist/esm/support/decorators/pool.js +83 -0
  104. package/dist/esm/support/decorators/types.d.ts +22 -0
  105. package/dist/esm/support/decorators/types.js +1 -0
  106. package/dist/esm/utils/invocation.d.ts +1 -0
  107. package/dist/esm/utils/invocation.js +1 -0
  108. package/package.json +10 -2
  109. package/dist/cjs/utils/debug.js +0 -30
  110. package/dist/esm/utils/debug.d.ts +0 -27
  111. package/dist/esm/utils/debug.js +0 -27
@@ -8,7 +8,7 @@
8
8
  * Issued loggers are tracked in a module-private registry, so only the execution that created
9
9
  * one can read it. A lookalike object with a `readAll` method is never accepted.
10
10
  *
11
- * Exports: `payloadSnapshot`, `createSubFlowLogger`, `readSubFlowLogger`.
11
+ * Exports: `payloadSnapshot`, `createSubFlowLogger`, `readSubFlowLogger`, `captureSubFlowSnapshot`.
12
12
  */
13
13
  import { breakWhile } from '../utils/rules.js';
14
14
  import { invocationId } from '../utils/invocation.js';
@@ -18,12 +18,29 @@ import { reportedStepStatusCatalog } from '../structs/debug.js';
18
18
  * private so a structurally identical object cannot impersonate a real logger.
19
19
  */
20
20
  const instances = new WeakMap();
21
+ /** Only recorder-issued snapshots may enter a diagnostic fact, within their original span. */
22
+ const snapshots = new WeakMap();
23
+ /** Validate a committed snapshot without consulting its logger or invoking App code. */
24
+ export function captureSubFlowSnapshot(steps, identity) {
25
+ const empty = { subSteps: Object.freeze([]), subStepsDropped: 0, subStepsTruncated: false };
26
+ if (steps === undefined)
27
+ return empty;
28
+ const scope = snapshots.get(steps);
29
+ if (!scope ||
30
+ (identity &&
31
+ (scope.identity?.runId !== identity.runId || scope.identity?.spanId !== identity.spanId)))
32
+ return { ...empty, subStepsDropped: 1 };
33
+ return { ...empty, subSteps: steps };
34
+ }
21
35
  /** Fields accepted by `write`; anything else is rejected so a declaration cannot smuggle state. */
22
36
  const writeFields = [
37
+ 'type',
38
+ 'subnodeId',
23
39
  'name',
24
40
  'kind',
25
41
  'parentStepId',
26
42
  'relatedDefinitionId',
43
+ 'relatedInvocationId',
27
44
  'status',
28
45
  'usedModel',
29
46
  'startedAtMs',
@@ -45,10 +62,13 @@ const fixFields = [
45
62
  ];
46
63
  /** Per-field character caps, bounding what one reported step can add to a persisted record. */
47
64
  const textLimits = {
65
+ type: 256,
66
+ subnodeId: 1024,
48
67
  name: 256,
49
68
  kind: 128,
50
69
  parentStepId: 256,
51
70
  relatedDefinitionId: 256,
71
+ relatedInvocationId: 256,
52
72
  caseId: 256,
53
73
  summary: 1024,
54
74
  };
@@ -170,6 +190,124 @@ function fill(previous, patch) {
170
190
  }
171
191
  return Object.freeze(result);
172
192
  }
193
+ /** Stable diagnostic reasons; untrusted error text and caller payloads never reach the console. */
194
+ const rejectionReasons = {
195
+ inactive: 'logger is detached or execution is cancelled',
196
+ scope: 'target does not belong to this subnode',
197
+ target: 'original write record was not found',
198
+ fields: 'input contains a forbidden field',
199
+ transition: 'status transition is not allowed',
200
+ sealed: 'an established fact cannot be overwritten',
201
+ limit: 'recording limit exceeded',
202
+ input: 'invalid input or result patch',
203
+ };
204
+ const rejectionMessages = {
205
+ 'Unknown sub-flow field': 'fields',
206
+ 'Unknown sub-flow step': 'target',
207
+ 'Invalid sub-flow status transition': 'transition',
208
+ 'Terminal sub-flow status is sealed': 'transition',
209
+ 'Terminal sub-flow model use is sealed': 'sealed',
210
+ 'Cannot undo reported model use': 'sealed',
211
+ 'Cannot overwrite a sub-flow time': 'sealed',
212
+ 'Cannot overwrite sub-flow successors': 'sealed',
213
+ 'Cannot overwrite a sub-flow summary': 'sealed',
214
+ 'Sub-flow log limit exceeded': 'limit',
215
+ };
216
+ /** Map only known own data messages; arbitrary thrown values are described generically. */
217
+ function rejectionReason(error) {
218
+ try {
219
+ const message = error instanceof Error ? Object.getOwnPropertyDescriptor(error, 'message')?.value : undefined;
220
+ return typeof message === 'string' && Object.hasOwn(rejectionMessages, message)
221
+ ? rejectionMessages[message]
222
+ : 'input';
223
+ }
224
+ catch {
225
+ return 'input';
226
+ }
227
+ }
228
+ /** Unexported issuance token prevents constructing counterfeit full or scoped loggers. */
229
+ const loggerToken = Symbol('sub-flow-logger');
230
+ const stores = new WeakMap();
231
+ /** Full execution-owned recorder. Owns its store and may issue restricted delegates. */
232
+ export class SubFlowLogger {
233
+ #store;
234
+ constructor(token, store) {
235
+ if (token !== loggerToken)
236
+ throw new TypeError('Use context.createSubFlowLogger()');
237
+ this.#store = store;
238
+ stores.set(this, store);
239
+ Object.freeze(this);
240
+ }
241
+ write(input) {
242
+ return this.#store.write(input);
243
+ }
244
+ fix(stepId, patch) {
245
+ return this.#store.fix(stepId, patch);
246
+ }
247
+ readAll() {
248
+ return this.#store.readAll();
249
+ }
250
+ /** Issue a delegate for an App-owned logical group; no execution or scheduling is implied. */
251
+ fork(subnodeId) {
252
+ if (typeof subnodeId !== 'string' || !subnodeId.trim() || subnodeId.length > 1024)
253
+ throw new TypeError('Invalid subnode id');
254
+ return new ScopedSubFlowLogger(loggerToken, this, subnodeId);
255
+ }
256
+ }
257
+ /** Non-inheriting delegate. Stores only the original logger and its bound subnode identity. */
258
+ export class ScopedSubFlowLogger {
259
+ #logger;
260
+ #subnodeId;
261
+ constructor(token, logger, subnodeId) {
262
+ if (token !== loggerToken)
263
+ throw new TypeError('Use logger.fork(subnodeId)');
264
+ this.#logger = logger;
265
+ this.#subnodeId = subnodeId;
266
+ Object.freeze(this);
267
+ }
268
+ write(input) {
269
+ if (!stores.get(this.#logger).isActive()) {
270
+ stores.get(this.#logger).warn('write', 'inactive');
271
+ return -1;
272
+ }
273
+ try {
274
+ fields(input, writeFields.filter((field) => field !== 'subnodeId'));
275
+ // Copy descriptors so private content getters remain subject to the original policy.
276
+ const bound = Object.defineProperties({}, Object.getOwnPropertyDescriptors(input));
277
+ Object.defineProperty(bound, 'subnodeId', { value: this.#subnodeId, enumerable: true });
278
+ return this.#logger.write(bound);
279
+ }
280
+ catch (error) {
281
+ stores.get(this.#logger).warn('write', rejectionReason(error));
282
+ return -1;
283
+ }
284
+ }
285
+ fix(stepId, patch) {
286
+ const store = stores.get(this.#logger);
287
+ if (!store.isActive()) {
288
+ store.warn('fix', 'inactive');
289
+ return false;
290
+ }
291
+ if (!store.owns(stepId, this.#subnodeId)) {
292
+ store.warn('fix', 'scope');
293
+ return false;
294
+ }
295
+ return this.#logger.fix(stepId, patch);
296
+ }
297
+ readAll() {
298
+ return this.#logger.readAll();
299
+ }
300
+ }
301
+ /** Runtime check complements nominal invocation types; scoped or forged inputs cannot invoke. */
302
+ export function requireSubFlowLogger(logger) {
303
+ if (logger !== undefined && (!(logger instanceof SubFlowLogger) || !instances.has(logger)))
304
+ throw new TypeError('Native invocation requires a full SubFlowLogger');
305
+ }
306
+ /** Allocate a short node-local group; the owning node visit already supplies the namespace. */
307
+ export function bindNativeSubFlowLogger(logger, kind, definitionId) {
308
+ requireSubFlowLogger(logger);
309
+ return logger.fork(stores.get(logger).nextNativeSubnodeId(kind, definitionId));
310
+ }
173
311
  /**
174
312
  * Create a logger bound to one active execution visit.
175
313
  *
@@ -178,51 +316,95 @@ function fill(previous, patch) {
178
316
  *
179
317
  * @param owner Object that owns the visit. `readSubFlowLogger` compares this identity before
180
318
  * returning any step.
181
- * @param active Reports whether the visit is still open; a closed logger rejects writes.
319
+ * @param active Reports whether the visit is still open; a detached logger returns write=-1 and fix=false.
182
320
  * @param content Reports whether content-level detail is enabled. When false, summaries and
183
321
  * payloads are dropped before anything is stored.
184
- * @param changed Called after each accepted write or fix with the progress delta to publish.
322
+ * @param changed Runtime receiver called after each accepted write/fix with its delta and immutable snapshot.
185
323
  * @param identity Diagnostic run and span the logger belongs to; used to reject cross-scope reads.
186
324
  * @returns A frozen logger instance.
187
325
  */
188
326
  export function createSubFlowLogger(owner, active, content, changed, identity) {
327
+ let root;
189
328
  class Logger {
190
329
  #steps = Object.freeze([]);
191
- #changes = 0;
330
+ #records = Object.freeze([]);
331
+ #warnings = new Set();
332
+ #nativeCalls = new Map();
192
333
  constructor() {
193
- instances.set(this, { owner, identity, read: () => this.#steps });
334
+ instances.set(this, {
335
+ owner,
336
+ identity,
337
+ read: () => this.#records,
338
+ detach: () => {
339
+ // Retained read-only instances must not keep the runtime or App consumers alive.
340
+ owner = {};
341
+ instances.get(this).owner = owner;
342
+ active = () => false;
343
+ content = () => false;
344
+ changed = () => { };
345
+ },
346
+ });
347
+ snapshots.set(this.#records, { identity });
194
348
  Object.freeze(this);
195
349
  }
196
- #assertActive() {
197
- // A logger that outlived its execution would append evidence to a finished request.
198
- breakWhile(!active(), () => new TypeError('Sub-flow logger belongs to a closed execution'));
199
- }
200
350
  #save(step, operation) {
201
- this.#assertActive();
202
- // A fix replaces the step with the same id in place; a write always appends.
351
+ if (!active())
352
+ return false;
353
+ // Logical heads validate revisions; only the immutable ledger is published or queried.
203
354
  const next = operation === 'write'
204
355
  ? [...this.#steps, step]
205
356
  : this.#steps.map((item) => (item.stepId === step.stepId ? step : item));
206
357
  // Successors must resolve inside this logger, otherwise the recorded graph would have
207
358
  // dangling edges.
208
359
  breakWhile(step.nextStepIds?.some((id) => !next.some((item) => item.stepId === id)), () => new TypeError('Sub-flow successors must belong to this logger'));
209
- // The step and size caps equal the persisted-record limits; the change cap bounds
210
- // write/fix churn even when the final step count stays small.
211
- breakWhile(next.length > 256 || this.#changes >= 1024 || JSON.stringify(next).length > 65536, () => new RangeError('Sub-flow log limit exceeded'));
212
- this.#steps = Object.freeze(next);
213
- this.#changes++;
214
- changed(this, {
360
+ const record = Object.freeze({
361
+ ...step,
215
362
  operation,
216
- stepId: step.stepId,
217
- sequence: step.sequence,
218
- revision: step.revision,
219
- createdAtMs: step.createdAtMs,
220
- status: step.status,
221
- ...(step.usedModel === undefined ? {} : { usedModel: step.usedModel }),
363
+ stepId: operation === 'write' ? step.stepId : invocationId(),
364
+ ...(operation === 'fix' ? { targetStepId: step.stepId } : {}),
365
+ sequence: this.#records.length + 1,
366
+ createdAtMs: Date.now(),
222
367
  });
368
+ const records = Object.freeze([...this.#records, record]);
369
+ breakWhile(next.length > 256 || records.length > 1024 || JSON.stringify(records).length > 65536, () => new RangeError('Sub-flow log limit exceeded'));
370
+ if (!active())
371
+ return false;
372
+ this.#steps = Object.freeze(next);
373
+ this.#records = records;
374
+ snapshots.set(records, { identity });
375
+ changed(root, {
376
+ operation,
377
+ type: record.type,
378
+ subnodeId: record.subnodeId,
379
+ relatedInvocationId: record.relatedInvocationId,
380
+ stepId: record.stepId,
381
+ ...(record.targetStepId ? { targetStepId: record.targetStepId } : {}),
382
+ sequence: record.sequence,
383
+ revision: record.revision,
384
+ createdAtMs: record.createdAtMs,
385
+ status: record.status,
386
+ ...(record.usedModel === undefined ? {} : { usedModel: record.usedModel }),
387
+ }, records);
388
+ return true;
223
389
  }
224
390
  write(input) {
225
- this.#assertActive();
391
+ if (!active()) {
392
+ this.warn('write', 'inactive');
393
+ return -1;
394
+ }
395
+ try {
396
+ const id = this.#append(input);
397
+ if (id === -1)
398
+ this.warn('write', 'inactive');
399
+ return id;
400
+ }
401
+ catch (error) {
402
+ this.warn('write', rejectionReason(error));
403
+ return -1;
404
+ }
405
+ }
406
+ /** Validate a new record before committing it; public write returns -1 on failure. */
407
+ #append(input) {
226
408
  fields(input, writeFields);
227
409
  breakWhile(input.nextStepIds !== undefined && !Array.isArray(input.nextStepIds), () => new TypeError('Invalid sub-flow successors'));
228
410
  const now = Date.now(), status = input.status === undefined ? 'planned' : input.status;
@@ -231,16 +413,21 @@ export function createSubFlowLogger(owner, active, content, changed, identity) {
231
413
  const step = {
232
414
  origin: 'reported',
233
415
  stepId: invocationId(),
234
- sequence: this.#steps.length + 1,
416
+ sequence: this.#records.length + 1,
235
417
  revision: 1,
236
418
  createdAtMs: now,
419
+ type: input.type ?? input.kind,
420
+ ...(input.subnodeId === undefined ? {} : { subnodeId: input.subnodeId }),
237
421
  name: input.name,
238
- kind: input.kind,
422
+ kind: input.kind ?? input.type,
239
423
  status,
240
424
  ...(input.parentStepId === undefined ? {} : { parentStepId: input.parentStepId }),
241
425
  ...(input.relatedDefinitionId === undefined
242
426
  ? {}
243
427
  : { relatedDefinitionId: input.relatedDefinitionId }),
428
+ ...(input.relatedInvocationId === undefined
429
+ ? {}
430
+ : { relatedInvocationId: input.relatedInvocationId }),
244
431
  ...(input.caseId === undefined ? {} : { caseId: input.caseId }),
245
432
  ...(input.usedModel === undefined ? {} : { usedModel: input.usedModel }),
246
433
  ...(input.startedAtMs === undefined && status !== 'running'
@@ -263,11 +450,30 @@ export function createSubFlowLogger(owner, active, content, changed, identity) {
263
450
  }
264
451
  validate(step);
265
452
  breakWhile(step.parentStepId && !this.#steps.some((item) => item.stepId === step.parentStepId), () => new TypeError('Sub-flow parent must belong to this logger'));
266
- this.#save(Object.freeze(step), 'write');
267
- return step.stepId;
453
+ return this.#save(Object.freeze(step), 'write') ? step.stepId : -1;
268
454
  }
269
455
  fix(stepId, patch) {
270
- this.#assertActive();
456
+ if (!active()) {
457
+ this.warn('fix', 'inactive');
458
+ return false;
459
+ }
460
+ if (stepId === -1) {
461
+ this.warn('fix', 'target');
462
+ return false;
463
+ }
464
+ try {
465
+ const fixed = this.#applyFix(stepId, patch);
466
+ if (!fixed)
467
+ this.warn('fix', 'inactive');
468
+ return fixed;
469
+ }
470
+ catch (error) {
471
+ this.warn('fix', rejectionReason(error));
472
+ return false;
473
+ }
474
+ }
475
+ /** Validate a revision completely before committing it; public fix reports failure as false. */
476
+ #applyFix(stepId, patch) {
271
477
  fields(patch, fixFields);
272
478
  const previous = this.#steps.find((step) => step.stepId === stepId);
273
479
  // The assertion above only satisfies the type checker; this check rejects an unknown id.
@@ -323,13 +529,42 @@ export function createSubFlowLogger(owner, active, content, changed, identity) {
323
529
  });
324
530
  }
325
531
  validate(step);
326
- this.#save(Object.freeze(step), 'fix');
532
+ return this.#save(Object.freeze(step), 'fix');
533
+ }
534
+ owns(stepId, subnodeId) {
535
+ return this.#steps.some((step) => step.stepId === stepId && step.subnodeId === subnodeId);
536
+ }
537
+ isActive() {
538
+ return active();
539
+ }
540
+ /** Count starts, including failed/cancelled calls; each node visit owns a fresh store. */
541
+ nextNativeSubnodeId(kind, definitionId) {
542
+ const key = JSON.stringify([kind, definitionId]);
543
+ const next = (this.#nativeCalls.get(key) ?? 0) + 1;
544
+ this.#nativeCalls.set(key, next);
545
+ return `${kind}:${encodeURIComponent(definitionId)}:${next}`;
546
+ }
547
+ /** Deduplicate safe developer warnings in the sole owning store, including delegated calls. */
548
+ warn(operation, reason) {
549
+ const key = `${operation}:${reason}`;
550
+ if (this.#warnings.has(key))
551
+ return;
552
+ this.#warnings.add(key);
553
+ try {
554
+ console.warn(`[agent-core:subflow] ${operation} rejected: ${rejectionReasons[reason]}`);
555
+ }
556
+ catch {
557
+ /* A console extension cannot change logging return values. */
558
+ }
327
559
  }
328
560
  readAll() {
329
- return this.#steps;
561
+ return this.#records;
330
562
  }
331
563
  }
332
- return new Logger();
564
+ const store = new Logger();
565
+ root = new SubFlowLogger(loggerToken, store);
566
+ instances.set(root, instances.get(store));
567
+ return root;
333
568
  }
334
569
  /**
335
570
  * Read the steps recorded by a logger issued by this module.
@@ -352,3 +587,7 @@ export function readSubFlowLogger(input, owner, identity) {
352
587
  instance.identity?.spanId !== identity.spanId), () => new TypeError('Sub-flow logger belongs to another diagnostic scope'));
353
588
  return instance.read();
354
589
  }
590
+ /** Release execution callbacks while preserving the instance's immutable query history. */
591
+ export function detachSubFlowLogger(logger) {
592
+ instances.get(logger)?.detach();
593
+ }
@@ -407,3 +407,4 @@ export declare class AgentCoreEngine<Result, State = unknown, App = undefined> {
407
407
  private errorCode;
408
408
  }
409
409
  export type { CoreStep, StepOutcome, LoopEvent, AgentCoreEngineOptions, } from '../structs/execution/engine.js';
410
+ export type * from '../structs/execution/workflow-views.js';
@@ -641,6 +641,7 @@ export class AgentCoreEngine {
641
641
  throw new WorkflowContractError('Prepared model call belongs to another execution or model');
642
642
  const runner = owner?.runner ?? runnerFor(request.modelId);
643
643
  return runner.invoke({
644
+ logger: request.logger,
644
645
  stageId: request.stageId,
645
646
  maxOutputTokens: request.maxOutputTokens,
646
647
  transport: request.transport,
@@ -880,14 +881,14 @@ export class AgentCoreEngine {
880
881
  try {
881
882
  const result = synchronous(report(), 'Workflow reports must be synchronous');
882
883
  reports?.receive(result);
883
- detail = { payload: structuredClone(result.payload), subs: result.subs };
884
+ detail = { payload: structuredClone(result.payload) };
884
885
  }
885
886
  catch {
886
887
  /* A report cannot suppress the workflow event. */
887
888
  }
888
889
  }
889
890
  reports?.close();
890
- diagnostics?.emit('workflow.event', () => ({ event, ...(detail?.payload === undefined ? {} : { payload: detail.payload }) }), reports?.debug(detail));
891
+ diagnostics?.emit('workflow.event', () => ({ event, ...(detail?.payload === undefined ? {} : { payload: detail.payload }) }), reports?.debug());
891
892
  return event;
892
893
  }
893
894
  /**
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Exports: `createEventScope` and `executionEvents`.
10
10
  */
11
- import { captureSubSteps } from '../utils/debug.js';
11
+ import { captureSubFlowSnapshot } from '../diagnostics/sub-flow.js';
12
12
  import { invocationId } from '../utils/invocation.js';
13
13
  import { getErrorInfo } from '../errors.js';
14
14
  import { coreEventCatalog } from '../diagnostics/catalog.js';
@@ -62,9 +62,7 @@ export function createEventScope(options = {}) {
62
62
  };
63
63
  const fact = (event, payload, debug) => {
64
64
  const value = payload();
65
- const reported = scope.enabled(event, 'content')
66
- ? captureSubSteps(debug?.subSteps, identity)
67
- : captureSubSteps(undefined);
65
+ const reported = captureSubFlowSnapshot(scope.enabled(event, 'content') ? debug?.recordedSubSteps : undefined, identity);
68
66
  return {
69
67
  ...identity,
70
68
  ...reported,
@@ -215,7 +213,9 @@ function metadataPayload(type, payload) {
215
213
  function eventSnapshot(record, content = true) {
216
214
  const metadata = () => ({
217
215
  ...record,
218
- ...captureSubSteps(undefined),
216
+ subSteps: [],
217
+ subStepsDropped: 0,
218
+ subStepsTruncated: false,
219
219
  payload: metadataPayload(record.type, record.payload),
220
220
  });
221
221
  try {
@@ -15,6 +15,7 @@ export declare class NodeReports implements NodeReportingContext {
15
15
  private active;
16
16
  private logger?;
17
17
  private usedModel?;
18
+ private steps;
18
19
  /**
19
20
  * @param diagnostics - Diagnostic scope of the visit; omitted when observation is off.
20
21
  * @param signal - Execution signal; an aborted signal suppresses all further reports.
@@ -33,16 +34,8 @@ export declare class NodeReports implements NodeReportingContext {
33
34
  * @throws TypeError When the report is not an object or its model-use flag conflicts.
34
35
  */
35
36
  receive(update: WorkflowNodeReport): void;
36
- /**
37
- * Build the debug view of this visit.
38
- *
39
- * Getters keep the values live while the fact is being built; `subSteps` prefers the
40
- * logger supplied in the report and falls back to the visit's own logger.
41
- *
42
- * @param update - Optional report whose logger replaces the visit logger.
43
- * @returns A debug output object consumed by the diagnostic pipeline.
44
- */
45
- debug(update?: WorkflowNodeReport): DebugStepsOutput;
37
+ /** Return the runtime-owned snapshot already received from write/fix. */
38
+ debug(): DebugStepsOutput;
46
39
  /**
47
40
  * Publish one progress report.
48
41
  *
@@ -56,8 +49,9 @@ export declare class NodeReports implements NodeReportingContext {
56
49
  /**
57
50
  * Create (once) the logger that records sub-steps for this visit.
58
51
  *
59
- * Sub-steps are only collected when progress or the terminal event carries content, so
60
- * a metadata-only observer pays no logging cost.
52
+ * Status metadata is retained for the active visit. Summary and payload capture require
53
+ * content permission from progress or the terminal event. The bound runtime receives
54
+ * every accepted revision before notifying consumers.
61
55
  *
62
56
  * @returns The visit's logger, identical on repeated calls.
63
57
  * @throws TypeError When the visit is already closed or the execution was aborted.
@@ -8,7 +8,7 @@
8
8
  * Exports: `NodeReports`.
9
9
  */
10
10
  import { breakWhile } from '../utils/rules.js';
11
- import { createSubFlowLogger, readSubFlowLogger } from '../diagnostics/sub-flow.js';
11
+ import { createSubFlowLogger, detachSubFlowLogger } from '../diagnostics/sub-flow.js';
12
12
  /**
13
13
  * The existing node report and diagnostic outlet, scoped to one execution visit.
14
14
  *
@@ -23,6 +23,7 @@ export class NodeReports {
23
23
  active = true;
24
24
  logger;
25
25
  usedModel;
26
+ steps = Object.freeze([]);
26
27
  /**
27
28
  * @param diagnostics - Diagnostic scope of the visit; omitted when observation is off.
28
29
  * @param signal - Execution signal; an aborted signal suppresses all further reports.
@@ -36,6 +37,9 @@ export class NodeReports {
36
37
  /** End the visit; later reports and logger creation are ignored or rejected. */
37
38
  close() {
38
39
  this.active = false;
40
+ if (this.logger)
41
+ detachSubFlowLogger(this.logger);
42
+ this.logger = undefined;
39
43
  }
40
44
  /**
41
45
  * Validate a report and retain its model-use flag.
@@ -48,34 +52,18 @@ export class NodeReports {
48
52
  */
49
53
  receive(update) {
50
54
  breakWhile(!update || typeof update !== 'object', () => new TypeError('Invalid node report'));
55
+ breakWhile(Object.hasOwn(update, 'subs'), () => new TypeError('Sub-flow logs are recorded automatically'));
51
56
  const usedModel = update.usedModel;
52
57
  breakWhile(usedModel !== undefined &&
53
58
  (typeof usedModel !== 'boolean' || (this.usedModel === true && !usedModel)), () => new TypeError('Invalid reported model use'));
54
59
  if (usedModel !== undefined)
55
60
  this.usedModel = usedModel;
56
61
  }
57
- /**
58
- * Build the debug view of this visit.
59
- *
60
- * Getters keep the values live while the fact is being built; `subSteps` prefers the
61
- * logger supplied in the report and falls back to the visit's own logger.
62
- *
63
- * @param update - Optional report whose logger replaces the visit logger.
64
- * @returns A debug output object consumed by the diagnostic pipeline.
65
- */
66
- debug(update) {
67
- const self = this;
62
+ /** Return the runtime-owned snapshot already received from write/fix. */
63
+ debug() {
68
64
  return {
69
- get usedModel() {
70
- return self.usedModel;
71
- },
72
- subSteps: () => {
73
- const supplied = update?.subs;
74
- const logger = supplied === undefined ? this.logger : supplied;
75
- if (logger !== undefined)
76
- readSubFlowLogger(logger, this);
77
- return logger;
78
- },
65
+ usedModel: this.usedModel,
66
+ recordedSubSteps: this.steps.length ? this.steps : undefined,
79
67
  };
80
68
  }
81
69
  /**
@@ -92,7 +80,7 @@ export class NodeReports {
92
80
  return;
93
81
  try {
94
82
  this.receive(update);
95
- this.diagnostics?.emit('node.progress', () => this.diagnostics?.enabled('node.progress', 'content') ? { payload: update.payload } : {}, this.debug(update));
83
+ this.diagnostics?.emit('node.progress', () => this.diagnostics?.enabled('node.progress', 'content') ? { payload: update.payload } : {}, this.debug());
96
84
  }
97
85
  catch {
98
86
  this.diagnostics?.emit('node.progress', () => ({ reportError: 'INVALID_NODE_REPORT' }));
@@ -101,8 +89,9 @@ export class NodeReports {
101
89
  /**
102
90
  * Create (once) the logger that records sub-steps for this visit.
103
91
  *
104
- * Sub-steps are only collected when progress or the terminal event carries content, so
105
- * a metadata-only observer pays no logging cost.
92
+ * Status metadata is retained for the active visit. Summary and payload capture require
93
+ * content permission from progress or the terminal event. The bound runtime receives
94
+ * every accepted revision before notifying consumers.
106
95
  *
107
96
  * @returns The visit's logger, identical on repeated calls.
108
97
  * @throws TypeError When the visit is already closed or the execution was aborted.
@@ -110,9 +99,10 @@ export class NodeReports {
110
99
  createSubFlowLogger() {
111
100
  breakWhile(!this.active || this.signal?.aborted, () => new TypeError('Cannot create a logger for a closed execution'));
112
101
  return (this.logger ??= createSubFlowLogger(this, () => this.active && !this.signal?.aborted, () => this.diagnostics?.enabled('node.progress', 'content') === true ||
113
- this.diagnostics?.enabled(this.terminalEvent, 'content') === true, (logger, progress) => this.diagnostics?.emit('node.progress', () => ({ subStep: progress }), {
114
- usedModel: this.usedModel,
115
- subSteps: () => logger,
116
- }), this.diagnostics?.identity));
102
+ this.diagnostics?.enabled(this.terminalEvent, 'content') === true, (_logger, progress, steps) => {
103
+ // Commit before notifying consumers; failed listeners cannot lose the accepted revision.
104
+ this.steps = steps;
105
+ this.diagnostics?.emit('node.progress', () => ({ subStep: progress }), this.debug());
106
+ }, this.diagnostics?.identity));
117
107
  }
118
108
  }
@@ -0,0 +1,3 @@
1
+ import type { WorkflowChain, WorkflowChainSource } from '../structs/execution/workflow-views.js';
2
+ /** Aggregate main visits by span and keep raw subflow records in their own append order. */
3
+ export declare function projectWorkflowChain(workflowId: string, runId: string, input: Awaited<ReturnType<WorkflowChainSource>>): WorkflowChain;