@dudousxd/nestjs-agent-core 0.21.0 → 0.22.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/README.md CHANGED
@@ -18,7 +18,8 @@ import type { ModelProvider, AgentStore, ToolSpec, RolesPolicy } from '@dudousxd
18
18
 
19
19
  ## Key types
20
20
 
21
- - `ToolSpec` — `{ name, kind: 'read' | 'action' | 'agent', description, inputSchema, roles?, ability?, targetAgent?, detached? }`. (`ToolKind` has three further members — `'ask'`, `'skill'` and `'memory'` — which no `ToolSpec` carries: they belong to the built-in tools the loop serves itself rather than from a handler. `ask` settles against a person, `skill` against the turn's journaled catalog, `remember` against the turn's journaled memory digest.) `inputSchema` is a [Standard Schema](https://standardschema.dev) (Zod, Valibot, ArkType); the loop validates via `~standard.validate`, throwing `ToolInputInvalidError` on failure.
21
+ - `ToolSpec` — `{ name, kind: 'read' | 'action' | 'agent', description, inputSchema, roles?, ability?, targetAgent?, detached?, terminal? }`. (`ToolKind` has three further members — `'ask'`, `'skill'` and `'memory'` — which no `ToolSpec` carries: they belong to the built-in tools the loop serves itself rather than from a handler. `ask` settles against a person, `skill` against the turn's journaled catalog, `remember` against the turn's journaled memory digest.) `inputSchema` is a [Standard Schema](https://standardschema.dev) (Zod, Valibot, ArkType); the loop validates via `~standard.validate`, throwing `ToolInputInvalidError` on failure.
22
+ - `AiToolCtx.emitUi(component, props, { id?, version? })` — **a tool pushes generative UI.** The `ui` frame streams live (inline, durable, and from a dispatched tool step's worker) and the component is persisted on the assistant message through the optional `AgentStore.setMessageUi`, once per step, after the step's tools settle. `id` defaults to `<toolCallId>:ui:<n>`, so a retried call replaces what it pushed. The pushes ride the tool step's journaled result (`wrapToolStepOutput` / `unwrapToolStepOutput`; the wrapper is only used when something was pushed), so a replay neither re-streams nor re-persists them. `ToolSpec.terminal` ends the turn after a successful call, settled in `persist:toolcall` like `detached`.
22
23
  - `AgentDefinition` — a named agent (`systemPrompt` string | `PromptBuilder`, `tools`, `delegatesTo`, `personas`, …) for multi-agent setups. `delegatesTo` holds `AgentDelegation` entries: a bare target name for the delegation that waits, `{ agent, detached: true }` for one that does not.
23
24
  - `detachedStarted` / `detachedDelivered` / `settleUnsettledDelegation` / `AgentLoopHooks.startAgent` / `AgentRunInput.deliverTo` — **delegation that does not block the chat.** An `agent`-kind call whose spec says `detached` is STARTED rather than awaited (`hooks.startAgent`, which the durable runner maps to `ctx.startChild` and the inline one to a loop nobody awaits), and the turn ends with a `DetachedDelegationReceipt` as the call's result instead of an answer. The started run carries a `deliverTo` address — the delegating thread and the call that started it — and posts its answer there as a message of its own, stamped with its own `runId` and `agentName`, so a client renders "the research agent finished" rather than the assistant's next reply. Whether a call detaches is settled INSIDE `persist:toolcall` alongside its kind and target, never from a live registry lookup, so a replay reads the branch back rather than re-deciding it; the loop writes the same checkpoint names either way, and only the runner's own positions (`spawn:` versus the awaited child's `signal:child:`) differ. A detached run streams into its OWN sink and its `action` tools park on its OWN run, so its approval reaches the pending-approvals surface instead of an inline card in whatever turn happens to be open. `settleUnsettledDelegation` is the runner's half: a run that crashed or was stopped posts a message saying so, because "started" is the one state a reader can neither wait on nor act on. `AgentRunInput.parentRunId` / `RecordRunStartInput.parentRunId` record the edge, so a delegation is not a run row with nothing pointing at it.
24
25
  - `RolesPolicy.can(actor, tool): boolean | Promise<boolean>` — the tool authorization seam
@@ -1,4 +1,4 @@
1
- import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-CwXibnce.cjs';
1
+ import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-BQqFt06V.cjs';
2
2
  import '@standard-schema/spec';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-CwXibnce.js';
1
+ import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-BQqFt06V.js';
2
2
  import '@standard-schema/spec';
3
3
 
4
4
  /**
package/dist/index.cjs CHANGED
@@ -97,6 +97,7 @@ __export(src_exports, {
97
97
  compositeSkillProvider: () => compositeSkillProvider,
98
98
  createFrameBuffer: () => createFrameBuffer,
99
99
  createIncrementalGate: () => createIncrementalGate,
100
+ createUiCollector: () => createUiCollector,
100
101
  dayBoundsUtc: () => dayBoundsUtc,
101
102
  decodeStreamEvent: () => decodeStreamEvent,
102
103
  defaultCanDecide: () => defaultCanDecide,
@@ -124,6 +125,7 @@ __export(src_exports, {
124
125
  mayDecideApproval: () => mayDecideApproval,
125
126
  memoryForgetVerdict: () => memoryForgetVerdict,
126
127
  memoryWriteVerdict: () => memoryWriteVerdict,
128
+ mergeUi: () => mergeUi,
127
129
  normalizeDelegation: () => normalizeDelegation,
128
130
  normalizeElicitationReply: () => normalizeElicitationReply,
129
131
  observeTurnFrames: () => observeTurnFrames,
@@ -174,6 +176,7 @@ __export(src_exports, {
174
176
  traceLlmTurn: () => traceLlmTurn,
175
177
  traceToolExecution: () => traceToolExecution,
176
178
  truncateDetailContent: () => truncateDetailContent,
179
+ unwrapToolStepOutput: () => unwrapToolStepOutput,
177
180
  validateElicitationAnswer: () => validateElicitationAnswer,
178
181
  validateElicitationValue: () => validateElicitationValue,
179
182
  validateStructured: () => validateStructured,
@@ -183,6 +186,7 @@ __export(src_exports, {
183
186
  withSkillTool: () => withSkillTool,
184
187
  withToolTimeout: () => withToolTimeout,
185
188
  withTurnFrames: () => withTurnFrames,
189
+ wrapToolStepOutput: () => wrapToolStepOutput,
186
190
  writeMemory: () => writeMemory
187
191
  });
188
192
  module.exports = __toCommonJS(src_exports);
@@ -343,6 +347,95 @@ function withTurnFrames(result, frames) {
343
347
  }
344
348
  __name(withTurnFrames, "withTurnFrames");
345
349
 
350
+ // src/tool-ui.ts
351
+ var TOOL_STEP_UI = "@@nestjs-agent/tool-step-ui";
352
+ function wrapToolStepOutput(output, ui) {
353
+ if (ui.length === 0) {
354
+ return output;
355
+ }
356
+ const wrapped = {
357
+ [TOOL_STEP_UI]: 1,
358
+ output,
359
+ ui: [
360
+ ...ui
361
+ ]
362
+ };
363
+ return wrapped;
364
+ }
365
+ __name(wrapToolStepOutput, "wrapToolStepOutput");
366
+ function unwrapToolStepOutput(raw) {
367
+ if (typeof raw === "object" && raw !== null && raw[TOOL_STEP_UI] === 1) {
368
+ const wrapped = raw;
369
+ return {
370
+ output: wrapped.output,
371
+ ui: Array.isArray(wrapped.ui) ? wrapped.ui : []
372
+ };
373
+ }
374
+ return {
375
+ output: raw,
376
+ ui: []
377
+ };
378
+ }
379
+ __name(unwrapToolStepOutput, "unwrapToolStepOutput");
380
+ function createUiCollector(toolCallId, write) {
381
+ const pushed = /* @__PURE__ */ new Map();
382
+ let next = 0;
383
+ const emit2 = /* @__PURE__ */ __name(async (component, props, options = {}) => {
384
+ if (typeof component !== "string" || component.length === 0) {
385
+ throw new Error("emitUi: component must be a non-empty string");
386
+ }
387
+ if (typeof props !== "object" || props === null || Array.isArray(props)) {
388
+ throw new Error("emitUi: props must be a JSON object");
389
+ }
390
+ let id = options.id;
391
+ if (id === void 0) {
392
+ id = `${toolCallId}:ui:${next}`;
393
+ next += 1;
394
+ }
395
+ const entry = {
396
+ id,
397
+ component,
398
+ // Snapshot: the frame and the persisted value are what the tool pushed at THIS moment, not
399
+ // whatever the object it handed over looks like when the step settles.
400
+ props: JSON.parse(JSON.stringify(props)),
401
+ ...options.version !== void 0 ? {
402
+ version: options.version
403
+ } : {},
404
+ toolCallId
405
+ };
406
+ pushed.set(id, entry);
407
+ await write?.({
408
+ kind: "ui",
409
+ ...entry
410
+ });
411
+ return {
412
+ id
413
+ };
414
+ }, "emit");
415
+ return {
416
+ emit: emit2,
417
+ components: /* @__PURE__ */ __name(() => [
418
+ ...pushed.values()
419
+ ], "components"),
420
+ restart: /* @__PURE__ */ __name(() => {
421
+ next = 0;
422
+ }, "restart")
423
+ };
424
+ }
425
+ __name(createUiCollector, "createUiCollector");
426
+ function mergeUi(...lists) {
427
+ const merged = /* @__PURE__ */ new Map();
428
+ for (const list of lists) {
429
+ for (const component of list ?? []) {
430
+ merged.set(component.id, component);
431
+ }
432
+ }
433
+ return [
434
+ ...merged.values()
435
+ ];
436
+ }
437
+ __name(mergeUi, "mergeUi");
438
+
346
439
  // src/spi/pricing-store.ts
347
440
  async function seedModelPrices(store, prices) {
348
441
  for (const price of prices) {
@@ -3357,6 +3450,11 @@ async function claimToolCall(turn, call) {
3357
3450
  ...spec?.detached === true ? {
3358
3451
  detached: true
3359
3452
  } : {},
3453
+ // Same for `terminal`: absent unless declared, so a deployment with no terminal tool writes
3454
+ // the bytes it always has, and a replay reads the branch back instead of re-deciding it.
3455
+ ...spec?.terminal === true ? {
3456
+ terminal: true
3457
+ } : {},
3360
3458
  ...approval !== void 0 ? {
3361
3459
  approval
3362
3460
  } : {}
@@ -3378,6 +3476,9 @@ async function claimToolCall(turn, call) {
3378
3476
  ...persisted?.detached === true ? {
3379
3477
  detached: true
3380
3478
  } : {},
3479
+ ...persisted?.terminal === true ? {
3480
+ terminal: true
3481
+ } : {},
3381
3482
  ...toolType === "action" && persisted?.approval !== void 0 ? {
3382
3483
  approval: persisted.approval
3383
3484
  } : {},
@@ -3544,7 +3645,7 @@ async function invokeClaimedTool(turn, claimed) {
3544
3645
  if (claimed.toolType === "memory") {
3545
3646
  return await rememberIntoTurn(turn, claimed, startedAt);
3546
3647
  }
3547
- let output;
3648
+ let raw;
3548
3649
  if (hooks.dispatchTool) {
3549
3650
  const stepCtx = {
3550
3651
  actor: input.actor,
@@ -3567,28 +3668,47 @@ async function invokeClaimedTool(turn, claimed) {
3567
3668
  } : {},
3568
3669
  // Numeric-only: the handler applies withToolTimeout AND its own local `classify` — see
3569
3670
  // ToolStepEnvelope.transientRetry.
3570
- transientRetry: resolveToolTransientRetryNumbers(deps.toolTransientRetry)
3671
+ transientRetry: resolveToolTransientRetryNumbers(deps.toolTransientRetry),
3672
+ // The handler hands back what the tool pushed through `ctx.emitUi` with its output.
3673
+ collectUi: true
3571
3674
  };
3572
- output = await hooks.dispatchTool(resolvedCall, envelope);
3675
+ raw = await hooks.dispatchTool(resolvedCall, envelope);
3573
3676
  } else {
3574
- const invocation = hooks.step(`tool:${call.id}`, () => traceToolExecution(hooks.runId, {
3575
- toolCallId: call.id,
3576
- toolName: call.name,
3577
- toolType
3578
- }, () => invokeWithTransientRetry(() => deps.registry.invoke(call.name, call.input, ctx, deps.rolesPolicy), deps.toolTransientRetry ?? {}, {
3579
- ...hooks.isControlFlowError !== void 0 ? {
3580
- isControlFlowError: hooks.isControlFlowError
3581
- } : {},
3582
- onRetry: /* @__PURE__ */ __name((attempt, retryError) => {
3583
- publishAgentToolRetry({
3584
- toolName: call.name,
3585
- toolCallId: call.id,
3586
- attempt,
3587
- message: retryError instanceof Error ? retryError.message : String(retryError)
3588
- });
3589
- }, "onRetry")
3590
- })));
3591
- output = deps.toolTimeoutMs !== void 0 ? await withToolTimeout(invocation, deps.toolTimeoutMs, call.name) : await invocation;
3677
+ const invocation = hooks.step(`tool:${call.id}`, async () => {
3678
+ const ui2 = createUiCollector(call.id, (event) => turn.writer.write(encodeStreamEvent(event)));
3679
+ const output2 = await traceToolExecution(hooks.runId, {
3680
+ toolCallId: call.id,
3681
+ toolName: call.name,
3682
+ toolType
3683
+ }, () => invokeWithTransientRetry(() => {
3684
+ ui2.restart();
3685
+ return deps.registry.invoke(call.name, call.input, {
3686
+ ...ctx,
3687
+ emitUi: ui2.emit
3688
+ }, deps.rolesPolicy);
3689
+ }, deps.toolTransientRetry ?? {}, {
3690
+ ...hooks.isControlFlowError !== void 0 ? {
3691
+ isControlFlowError: hooks.isControlFlowError
3692
+ } : {},
3693
+ onRetry: /* @__PURE__ */ __name((attempt, retryError) => {
3694
+ publishAgentToolRetry({
3695
+ toolName: call.name,
3696
+ toolCallId: call.id,
3697
+ attempt,
3698
+ message: retryError instanceof Error ? retryError.message : String(retryError)
3699
+ });
3700
+ }, "onRetry")
3701
+ }));
3702
+ return wrapToolStepOutput(output2, ui2.components());
3703
+ });
3704
+ raw = deps.toolTimeoutMs !== void 0 ? await withToolTimeout(invocation, deps.toolTimeoutMs, call.name) : await invocation;
3705
+ }
3706
+ const { output, ui } = unwrapToolStepOutput(raw);
3707
+ if (ui.length > 0) {
3708
+ turn.toolUi.set(call.id, ui);
3709
+ }
3710
+ if (claimed.terminal === true) {
3711
+ turn.halt.terminal = true;
3592
3712
  }
3593
3713
  return {
3594
3714
  status: "executed",
@@ -4486,6 +4606,10 @@ ${block}`;
4486
4606
  hooks,
4487
4607
  messageId: assistant.id,
4488
4608
  writer,
4609
+ toolUi: /* @__PURE__ */ new Map(),
4610
+ halt: {
4611
+ terminal: false
4612
+ },
4489
4613
  ...skillOffer !== void 0 ? {
4490
4614
  skills: skillOffer
4491
4615
  } : {},
@@ -4518,6 +4642,10 @@ ${block}`;
4518
4642
  assistantMessage.toolResults = settledResults;
4519
4643
  await hooks.step(`stream:tool-outputs:${i}`, async () => {
4520
4644
  await deps.store.setMessageToolResults(assistant.id, settledResults);
4645
+ const toolUi = toolCallsWithKind.flatMap((call) => turnCalls.toolUi.get(call.id) ?? []);
4646
+ if (toolUi.length > 0) {
4647
+ await deps.store.setMessageUi?.(assistant.id, mergeUi(turn.ui, toolUi));
4648
+ }
4521
4649
  for (const result of results) {
4522
4650
  await writer.write(encodeStreamEvent(outputFrame(result)));
4523
4651
  }
@@ -4532,6 +4660,9 @@ ${block}`;
4532
4660
  } : {}
4533
4661
  }));
4534
4662
  });
4663
+ if (turnCalls.halt.terminal) {
4664
+ break;
4665
+ }
4535
4666
  }
4536
4667
  if (history.title === "" || history.title === "New chat") {
4537
4668
  await hooks.step("persist:title", () => deps.store.setTitle(input.threadId, deriveTitle(input.userText)));
@@ -4662,6 +4793,7 @@ __name(runAgentLoop, "runAgentLoop");
4662
4793
  compositeSkillProvider,
4663
4794
  createFrameBuffer,
4664
4795
  createIncrementalGate,
4796
+ createUiCollector,
4665
4797
  dayBoundsUtc,
4666
4798
  decodeStreamEvent,
4667
4799
  defaultCanDecide,
@@ -4689,6 +4821,7 @@ __name(runAgentLoop, "runAgentLoop");
4689
4821
  mayDecideApproval,
4690
4822
  memoryForgetVerdict,
4691
4823
  memoryWriteVerdict,
4824
+ mergeUi,
4692
4825
  normalizeDelegation,
4693
4826
  normalizeElicitationReply,
4694
4827
  observeTurnFrames,
@@ -4739,6 +4872,7 @@ __name(runAgentLoop, "runAgentLoop");
4739
4872
  traceLlmTurn,
4740
4873
  traceToolExecution,
4741
4874
  truncateDetailContent,
4875
+ unwrapToolStepOutput,
4742
4876
  validateElicitationAnswer,
4743
4877
  validateElicitationValue,
4744
4878
  validateStructured,
@@ -4748,6 +4882,7 @@ __name(runAgentLoop, "runAgentLoop");
4748
4882
  withSkillTool,
4749
4883
  withToolTimeout,
4750
4884
  withTurnFrames,
4885
+ wrapToolStepOutput,
4751
4886
  writeMemory
4752
4887
  });
4753
4888
  //# sourceMappingURL=index.cjs.map