@noetic-tools/context 1.8.0 → 1.10.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.
@@ -1,4 +1,4 @@
1
- import type { ContextLayer, DecisionRecord, ExecutionContext, Item, ItemLog, ItemSchemaRegistry, LLMResponse, ScopedStorage, SteeringDecision, StorageAdapter } from '@noetic-tools/types';
1
+ import type { ContextLayer, DecisionRecord, ExecutionContext, Item, ItemLog, ItemSchemaRegistry, LLMResponse, OnSteerParams, ScopedStorage, SteeringDecision, SteerVerdict, StorageAdapter } from '@noetic-tools/types';
2
2
  import type { LiveChild } from './parent-updates.js';
3
3
  export interface LayerStateStore {
4
4
  get<T>(executionId: string, layerId: string): T | undefined;
@@ -154,6 +154,11 @@ interface AfterModelCallLayersParams {
154
154
  ctx: ExecutionContext;
155
155
  store: LayerStateStore;
156
156
  }
157
+ interface OnSteerLayersParams extends Pick<OnSteerParams, 'steer' | 'point' | 'action' | 'pendingCalls'> {
158
+ layers: ContextLayer[];
159
+ ctx: ExecutionContext;
160
+ store: LayerStateStore;
161
+ }
157
162
  interface ProjectHistoryLayersParams {
158
163
  layers: ContextLayer[];
159
164
  items: ReadonlyArray<Item>;
@@ -274,6 +279,12 @@ export declare function returnLayers<T>({ layers, stepId, parentCtx, childCtx, c
274
279
  export declare function completeLayers({ layers, ctx, log, outcome, store, }: CompleteLayersParams): Promise<void>;
275
280
  export declare function beforeToolCallLayers({ layers, toolName, toolArgs, callId, ctx, store, }: BeforeToolCallLayersParams): Promise<SteeringDecision>;
276
281
  export declare function afterModelCallLayers({ layers, response, ctx, store, }: AfterModelCallLayersParams): Promise<SteeringDecision>;
282
+ /**
283
+ * Run a steer through every layer's `onSteer` hook in slot order. Each layer
284
+ * sees the action and items the previous layer left, so the last layer to
285
+ * speak wins. A hook that throws or times out leaves both unchanged.
286
+ */
287
+ export declare function onSteerLayers({ layers, steer, point, action, pendingCalls, ctx, store, }: OnSteerLayersParams): Promise<SteerVerdict>;
277
288
  /**
278
289
  * Run history items through every layer's `projectHistory` hook in slot order.
279
290
  * Each layer receives the previous layer's output. Storage (`itemLog`) is
@@ -889,6 +889,70 @@ export async function afterModelCallLayers({ layers, response, ctx, store, }) {
889
889
  }
890
890
  return mostRestrictive(decisions);
891
891
  }
892
+ /**
893
+ * Run a steer through every layer's `onSteer` hook in slot order. Each layer
894
+ * sees the action and items the previous layer left, so the last layer to
895
+ * speak wins. A hook that throws or times out leaves both unchanged.
896
+ */
897
+ export async function onSteerLayers({ layers, steer, point, action, pendingCalls, ctx, store, }) {
898
+ const sorted = [
899
+ ...layers,
900
+ ].sort((a, b) => a.slot - b.slot);
901
+ let verdict = {
902
+ action,
903
+ items: steer.items,
904
+ };
905
+ for (const layer of sorted) {
906
+ if (!layer.hooks.onSteer) {
907
+ continue;
908
+ }
909
+ const state = store.get(ctx.executionId, layer.id);
910
+ if (isLayerDisabled({
911
+ store,
912
+ executionId: ctx.executionId,
913
+ layer,
914
+ state,
915
+ })) {
916
+ await traceSkipped(ctx, layer.id, 'onSteer');
917
+ continue;
918
+ }
919
+ const onSteer = layer.hooks.onSteer;
920
+ const current = verdict;
921
+ try {
922
+ const result = await callHook({
923
+ ctx,
924
+ layerId: layer.id,
925
+ hook: 'onSteer',
926
+ timeout: layer.timeouts?.onSteer ?? 5e3,
927
+ run: () => onSteer({
928
+ steer: {
929
+ id: steer.id,
930
+ items: current.items,
931
+ },
932
+ point,
933
+ action: current.action,
934
+ pendingCalls,
935
+ ctx,
936
+ state,
937
+ }),
938
+ });
939
+ if (result.state !== undefined) {
940
+ store.set(ctx.executionId, layer.id, result.state);
941
+ }
942
+ verdict = {
943
+ action: result.action ?? current.action,
944
+ items: result.items ?? current.items,
945
+ };
946
+ }
947
+ catch (e) {
948
+ if (e instanceof NoeticConfigError) {
949
+ throw e;
950
+ }
951
+ store.diagnostic(layer.id, 'onSteer', e);
952
+ }
953
+ }
954
+ return verdict;
955
+ }
892
956
  /**
893
957
  * Run history items through every layer's `projectHistory` hook in slot order.
894
958
  * Each layer receives the previous layer's output. Storage (`itemLog`) is
@@ -290,7 +290,7 @@ ${structure.join('\n')}
290
290
 
291
291
  **Shaping it**
292
292
  - Keep the tree the user reviews SMALL — about seven top-level nodes, readable at a glance. Push the mechanics down.
293
- - Factor detail into named workflows: define each with \`plan/setWorkflow\` \`{ "name": "<slug>", "document": {...} }\` and point at it with \`{ "kind": "subflow", "id": "...", "ref": "<slug>" }\`. Named workflows may reference each other but must not form a cycle, and every ref must resolve or the exit is rejected.
293
+ - Factor detail into named workflows: define each with \`plan/setWorkflow\` \`{ "name": "<slug>", "document": {...} }\` and point at it with \`{ "kind": "subflow", "id": "...", "ref": "<slug>" }\`. Named workflows may reference each other, and may recurse (a workflow may call itself) as long as a branch eventually stops the recursion; every ref must resolve or the exit is rejected.
294
294
  - Fan out work that is genuinely independent; sequence work where one step needs the last one's output. Do not fan out for the look of it.
295
295
  - Prefer \`invokeTool\` over \`callModel\` wherever the step is deterministic. A model turn that always does the same thing is a slow, expensive tool call.
296
296
  - Always give a \`loop\` a \`maxIterations\`. Without one it stops at a hard ceiling of 1000 and fails the step.`;
@@ -161,49 +161,6 @@ function findDanglingRefs(state) {
161
161
  }
162
162
  return dangling;
163
163
  }
164
- /**
165
- * Detects a reference cycle among named workflows (self-reference included).
166
- * Returns the cycle path for the rejection message, or null when acyclic.
167
- */
168
- function findWorkflowCycle(workflows) {
169
- const edges = new Map();
170
- for (const [name, doc] of Object.entries(workflows)) {
171
- edges.set(name, collectSubflowRefs(doc).filter((ref) => ref in workflows));
172
- }
173
- const visiting = new Set();
174
- const done = new Set();
175
- const path = [];
176
- const visit = (name) => {
177
- if (done.has(name)) {
178
- return null;
179
- }
180
- if (visiting.has(name)) {
181
- return [
182
- ...path.slice(path.indexOf(name)),
183
- name,
184
- ];
185
- }
186
- visiting.add(name);
187
- path.push(name);
188
- for (const next of edges.get(name) ?? []) {
189
- const cycle = visit(next);
190
- if (cycle) {
191
- return cycle;
192
- }
193
- }
194
- path.pop();
195
- visiting.delete(name);
196
- done.add(name);
197
- return null;
198
- };
199
- for (const name of edges.keys()) {
200
- const cycle = visit(name);
201
- if (cycle) {
202
- return cycle;
203
- }
204
- }
205
- return null;
206
- }
207
164
  function buildAllowedTools(config) {
208
165
  if (!config?.additionalAllowedTools?.length) {
209
166
  return ALLOWED_TOOLS_IN_PLAN_MODE;
@@ -462,13 +419,6 @@ async function executeExitPlanMode(args, state, options) {
462
419
  state,
463
420
  };
464
421
  }
465
- const cycle = findWorkflowCycle(state.workflows);
466
- if (cycle) {
467
- return {
468
- result: `Cannot execute: workflows reference each other in a cycle: ${cycle.join(' -> ')}. Break the cycle before exiting.`,
469
- state,
470
- };
471
- }
472
422
  if (options.onExit) {
473
423
  const { approved } = await options.onExit(state);
474
424
  if (!approved) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noetic-tools/context",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "type": "module",
5
5
  "description": "Context layer system for Noetic agents: the ContextLayer contract, lifecycle, budget/projection machinery, and built-in layer implementations.",
6
6
  "license": "Apache-2.0",
@@ -44,7 +44,7 @@
44
44
  "zod": "^4.0.0"
45
45
  },
46
46
  "dependencies": {
47
- "@noetic-tools/types": "^3.9.0",
47
+ "@noetic-tools/types": "^3.13.0",
48
48
  "zod": "^4.0.0"
49
49
  },
50
50
  "scripts": {