@noetic-tools/core 5.4.2 → 5.5.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.
@@ -308,7 +308,7 @@ export async function executeToolCall(params) {
308
308
  };
309
309
  }
310
310
  if (params.layers && params.layers.length > 0) {
311
- const decision = await params.harness.beforeToolCall(params.layers, params.toolName, params.args, params.context);
311
+ const decision = await params.harness.beforeToolCall(params.layers, params.toolName, params.args, params.context, params.callId);
312
312
  if (decision.action === SteeringAction.Deny) {
313
313
  return {
314
314
  output: `Tool call denied: ${decision.guidance ?? 'steering rule violation'}`,
@@ -1,4 +1,4 @@
1
- import type { FunctionCallItem, FunctionCallOutputItem, InferSchemaOutput, InputSchemaConfig, Item, ItemSchemaExtensions, StandardSchemaV1, Tool, ToolContextDeclaration, ToolExecutionContext, ToolResultExtensionItem, ToolUiDeclaration } from '@noetic-tools/types';
1
+ import type { FunctionCallItem, FunctionCallOutputItem, InferSchemaOutput, InputSchemaConfig, Item, ItemSchemaExtensions, StandardSchemaV1, Tool, ToolAcpDeclaration, ToolContextDeclaration, ToolExecutionContext, ToolResultExtensionItem, ToolUiDeclaration } from '@noetic-tools/types';
2
2
  type ToolConfig<I extends StandardSchemaV1, O extends StandardSchemaV1> = {
3
3
  name: string;
4
4
  description: string;
@@ -19,6 +19,8 @@ type ToolConfig<I extends StandardSchemaV1, O extends StandardSchemaV1> = {
19
19
  context?: ToolContextDeclaration;
20
20
  /** Optional UI declaration — the runtime emits the rendered fragments at call/progress/result points. */
21
21
  ui?: ToolUiDeclaration<I, O>;
22
+ /** Optional ACP presentation — how the call renders in an ACP client when the harness is served as an agent. */
23
+ acp?: ToolAcpDeclaration<I>;
22
24
  } & InputSchemaConfig<I>;
23
25
  type GeneratorToolConfig<I extends StandardSchemaV1, E extends StandardSchemaV1, O extends StandardSchemaV1> = {
24
26
  name: string;
@@ -41,6 +43,8 @@ type GeneratorToolConfig<I extends StandardSchemaV1, E extends StandardSchemaV1,
41
43
  context?: ToolContextDeclaration;
42
44
  /** Optional UI declaration — the runtime emits the rendered fragments at call/progress/result points. */
43
45
  ui?: ToolUiDeclaration<I, O, InferSchemaOutput<E>>;
46
+ /** Optional ACP presentation — how the call renders in an ACP client when the harness is served as an agent. */
47
+ acp?: ToolAcpDeclaration<I>;
44
48
  } & InputSchemaConfig<I>;
45
49
  /**
46
50
  * Creates a typed Tool with Standard Schema inference for input and output.
@@ -39,6 +39,7 @@ export function tool(config) {
39
39
  needsApproval: config.needsApproval,
40
40
  context: config.context,
41
41
  ui: config.ui,
42
+ acp: config.acp,
42
43
  };
43
44
  }
44
45
  /**
@@ -61,6 +62,7 @@ export function toolWithGenerator(config) {
61
62
  needsApproval: config.needsApproval,
62
63
  context: config.context,
63
64
  ui: config.ui,
65
+ acp: config.acp,
64
66
  };
65
67
  }
66
68
  //#endregion
@@ -271,6 +271,8 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
271
271
  threadId?: string;
272
272
  resourceId?: string;
273
273
  contextLayers?: ContextLayer[];
274
+ /** Additional layers appended AFTER the effective set — never replaces it. */
275
+ extraContextLayers?: ContextLayer[];
274
276
  /**
275
277
  * Initial cwd for the new context. When set, takes precedence over both
276
278
  * the parent snapshot and the harness root cwd — used by worktree
@@ -393,7 +395,7 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
393
395
  createSpan(name: string, parent: Span | null): Span;
394
396
  getLayerState<T>(executionId: string, layerId: string): T | undefined;
395
397
  setLayerState<T>(executionId: string, layerId: string, state: T): void;
396
- beforeToolCall(layers: ContextLayer[], toolName: string, toolArgs: unknown, ctx: Context): Promise<SteeringDecision>;
398
+ beforeToolCall(layers: ContextLayer[], toolName: string, toolArgs: unknown, ctx: Context, callId?: string): Promise<SteeringDecision>;
397
399
  afterModelCall(layers: ContextLayer[], response: LLMResponse, ctx: Context): Promise<SteeringDecision>;
398
400
  projectHistory(layers: ContextLayer[], items: ReadonlyArray<Item>, ctx: Context): Promise<ReadonlyArray<Item>>;
399
401
  runAppendPipeline(layers: ContextLayer[], items: Item[], ctx: Context): Promise<{
@@ -439,6 +439,7 @@ export class AgentHarness {
439
439
  resourceId: perTurnOptions.resourceId,
440
440
  state: perTurnOptions.state,
441
441
  contextLayers: perTurnOptions.contextLayers,
442
+ extraContextLayers: perTurnOptions.extraContextLayers,
442
443
  _broadcaster: session.runner.broadcaster,
443
444
  });
444
445
  const ext = frameworkCast(ctx);
@@ -596,8 +597,14 @@ export class AgentHarness {
596
597
  const resolved = opts ?? {};
597
598
  // `ContextImpl` takes the layers as `layers`, so the option spelling is
598
599
  // dropped from `rest` rather than spread through under the wrong name.
599
- const { contextLayers, cwdInit, ...rest } = resolved;
600
- const effectiveLayers = contextLayers ?? this._contextLayers;
600
+ const { contextLayers, extraContextLayers, cwdInit, ...rest } = resolved;
601
+ const baseLayers = contextLayers ?? this._contextLayers;
602
+ const effectiveLayers = extraContextLayers && extraContextLayers.length > 0
603
+ ? [
604
+ ...(baseLayers ?? []),
605
+ ...extraContextLayers,
606
+ ]
607
+ : baseLayers;
601
608
  const itemSchemas = buildItemSchemaRegistry({
602
609
  base: this.itemSchemas,
603
610
  layers: effectiveLayers,
@@ -948,7 +955,7 @@ export class AgentHarness {
948
955
  setLayerState(executionId, layerId, state) {
949
956
  this.layerStateStore.set(executionId, layerId, state);
950
957
  }
951
- async beforeToolCall(layers, toolName, toolArgs, ctx) {
958
+ async beforeToolCall(layers, toolName, toolArgs, ctx, callId) {
952
959
  const hasHook = layers.some((l) => l.hooks.beforeToolCall);
953
960
  if (!hasHook) {
954
961
  return {
@@ -959,6 +966,7 @@ export class AgentHarness {
959
966
  layers,
960
967
  toolName,
961
968
  toolArgs,
969
+ callId,
962
970
  ctx: this.toExecCtx(ctx),
963
971
  store: this.layerStateStore,
964
972
  });
@@ -749,7 +749,8 @@ export class AgentHarnessModelCaller {
749
749
  params.emitIfAllowed('tool_call_completed', {
750
750
  name: fc.name,
751
751
  callId: fc.callId,
752
- error: false,
752
+ error: toolResult.error === true,
753
+ output: toolResult.output,
753
754
  });
754
755
  }
755
756
  recordMalformedToolCall(params) {
@@ -776,6 +777,7 @@ export class AgentHarnessModelCaller {
776
777
  name: fc.name,
777
778
  callId: fc.callId,
778
779
  error: true,
780
+ output: errorOutput,
779
781
  });
780
782
  }
781
783
  handleToolRoundLimit(params) {
@@ -132,7 +132,7 @@ function buildSteerer(ctx, layers) {
132
132
  return undefined;
133
133
  }
134
134
  return async (request) => {
135
- const decision = await ctx.harness.beforeToolCall(layers, request.toolCall.title ?? request.toolCall.toolCallId, request.toolCall.rawInput, ctx);
135
+ const decision = await ctx.harness.beforeToolCall(layers, request.toolCall.title ?? request.toolCall.toolCallId, request.toolCall.rawInput, ctx, request.toolCall.toolCallId);
136
136
  if (decision.action === SteeringAction.Allow) {
137
137
  return undefined;
138
138
  }
@@ -386,26 +386,11 @@ function buildModelRequest(params) {
386
386
  const { step, ctx, layers } = params;
387
387
  const _serverTools = params.serverToolSpecs.length > 0 ? params.serverToolSpecs : undefined;
388
388
  const signal = isContextImpl(ctx) ? ctx.abortSignal : undefined;
389
- if (params.resolvedTools) {
390
- return {
391
- model: params.resolvedModel,
392
- items: params.assembledItems,
393
- instructions: params.resolvedInstructions,
394
- tools: params.resolvedTools,
395
- params: step.params,
396
- outputSchema: params.outputSchema,
397
- outputJsonSchema: params.step.outputJsonSchema,
398
- emit: step.emit,
399
- _serverTools,
400
- ctx,
401
- layers,
402
- allowedToolNames: params.allowedToolNames,
403
- nodeId: step.id,
404
- parentSpan: ctx.span,
405
- signal,
406
- };
407
- }
408
- return {
389
+ // One construction for both shapes: the tool fields vary, but `ctx` (which
390
+ // carries the broadcaster — without it a tool-less call streams NOTHING to
391
+ // getFullStream/getTextStream) and `layers` must be attached either way.
392
+ // The two hand-maintained branches this replaces had drifted exactly there.
393
+ const base = {
409
394
  model: params.resolvedModel,
410
395
  items: params.assembledItems,
411
396
  instructions: params.resolvedInstructions,
@@ -414,10 +399,20 @@ function buildModelRequest(params) {
414
399
  outputJsonSchema: params.step.outputJsonSchema,
415
400
  emit: step.emit,
416
401
  _serverTools,
402
+ ctx,
403
+ layers,
417
404
  nodeId: step.id,
418
405
  parentSpan: ctx.span,
419
406
  signal,
420
407
  };
408
+ if (params.resolvedTools) {
409
+ return {
410
+ ...base,
411
+ tools: params.resolvedTools,
412
+ allowedToolNames: params.allowedToolNames,
413
+ };
414
+ }
415
+ return base;
421
416
  }
422
417
  /**
423
418
  * Records a finished model response on the context: appends response items to
@@ -881,9 +876,37 @@ export async function executeInvokeTool(step, input, ctx, harness, layers) {
881
876
  });
882
877
  }
883
878
  const parsedArgs = parseResult.value;
879
+ // The started event precedes the gate so a parked call (e.g. one awaiting an
880
+ // external permission decision) is visible as pending rather than invisible
881
+ // until approved — the same ordering the model-driven path guarantees.
882
+ const broadcaster = getBroadcaster(baseCtx);
883
+ const agentName = baseCtx.harness.config.name;
884
+ emitFrameworkEvent({
885
+ broadcaster,
886
+ agentName,
887
+ eventType: 'tool_call_started',
888
+ data: {
889
+ name: step.tool.name,
890
+ callId: step.id,
891
+ },
892
+ });
893
+ const emitCompleted = (error, output) => {
894
+ emitFrameworkEvent({
895
+ broadcaster,
896
+ agentName,
897
+ eventType: 'tool_call_completed',
898
+ data: {
899
+ name: step.tool.name,
900
+ callId: step.id,
901
+ error,
902
+ output,
903
+ },
904
+ });
905
+ };
884
906
  if (layers && layers.length > 0) {
885
- const decision = await harness.beforeToolCall(layers, step.tool.name, parsedArgs, baseCtx);
907
+ const decision = await harness.beforeToolCall(layers, step.tool.name, parsedArgs, baseCtx, step.id);
886
908
  if (decision.action !== SteeringAction.Allow) {
909
+ emitCompleted(true, `Tool call denied: ${decision.guidance ?? 'steering rule violation'}`);
887
910
  throw new NoeticErrorImpl({
888
911
  kind: 'steering_denied',
889
912
  guidance: decision.guidance,
@@ -917,6 +940,7 @@ export async function executeInvokeTool(step, input, ctx, harness, layers) {
917
940
  args: parsedArgs,
918
941
  output: result,
919
942
  });
943
+ emitCompleted(false, typeof result === 'string' ? result : JSON.stringify(result));
920
944
  return frameworkCast(result);
921
945
  }
922
946
  catch (e) {
@@ -928,6 +952,7 @@ export async function executeInvokeTool(step, input, ctx, harness, layers) {
928
952
  args: parsedArgs,
929
953
  error: e,
930
954
  });
955
+ emitCompleted(true, e instanceof Error ? e.message : String(e));
931
956
  if (e instanceof NoeticErrorImpl) {
932
957
  throw e;
933
958
  }
@@ -1,3 +1,4 @@
1
+ import { isNoeticError } from '@noetic-tools/types';
1
2
  import { emitFrameworkEvent } from './broadcaster-utils.js';
2
3
  import { EventBroadcaster } from './event-broadcaster.js';
3
4
  import { MessageQueue } from './message-queue.js';
@@ -240,6 +241,9 @@ export class SessionRunner {
240
241
  data: {
241
242
  turnId,
242
243
  reason: error.message,
244
+ // The typed kind ('cancelled', 'model_refused', …) so consumers can
245
+ // classify the abort without parsing the human-readable message.
246
+ errorKind: isNoeticError(error) ? error.noeticError.kind : undefined,
243
247
  },
244
248
  });
245
249
  this.rejectWaiters(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noetic-tools/core",
3
- "version": "5.4.2",
3
+ "version": "5.5.0",
4
4
  "type": "module",
5
5
  "description": "Step primitives, interpreter, runtime, and context layer contract for Noetic agents.",
6
6
  "license": "Apache-2.0",
@@ -68,8 +68,8 @@
68
68
  }
69
69
  },
70
70
  "dependencies": {
71
- "@noetic-tools/context": "^1.2.2",
72
- "@noetic-tools/types": "^3.2.2",
71
+ "@noetic-tools/context": "^1.3.0",
72
+ "@noetic-tools/types": "^3.3.0",
73
73
  "@openrouter/agent": "^0.6.0",
74
74
  "zod": "^4.0.0"
75
75
  },