@noetic-tools/core 1.1.0 → 1.1.2
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/builders/step-builders.d.ts +51 -1
- package/dist/builders/step-builders.js +81 -0
- package/dist/builders/workflow-hydrator.d.ts +3 -1
- package/dist/builders/workflow-hydrator.js +34 -0
- package/dist/harness/agent-harness.d.ts +8 -1
- package/dist/harness/agent-harness.js +7 -0
- package/dist/harness/deps/types.d.ts +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +2 -0
- package/dist/interpreter/collect-tools.js +6 -0
- package/dist/interpreter/execute-action.d.ts +10 -0
- package/dist/interpreter/execute-action.js +1 -1
- package/dist/interpreter/execute-sub-harness.d.ts +8 -0
- package/dist/interpreter/execute-sub-harness.js +220 -0
- package/dist/interpreter/execute.js +22 -1
- package/dist/interpreter/sub-harness-events.d.ts +48 -0
- package/dist/interpreter/sub-harness-events.js +208 -0
- package/dist/patterns/dynamic-workflow.js +3 -0
- package/dist/schemas/workflow.d.ts +35 -2
- package/dist/schemas/workflow.js +46 -1
- package/package.json +2 -2
- package/schema/noetic-workflow.schema.json +320 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContextMemory } from '@noetic-tools/memory';
|
|
2
|
-
import type { Context, Lazy, ModelParams, RetryPolicy, StepLLM, StepRun, StepTool, SubprocessAdapter, Tool } from '@noetic-tools/types';
|
|
2
|
+
import type { Context, Lazy, ModelParams, RetryPolicy, StepLLM, StepRun, StepSubHarness, StepTool, SubHarness, SubHarnessSessionPolicy, SubHarnessSettings, SubprocessAdapter, Tool } from '@noetic-tools/types';
|
|
3
3
|
import type { ZodType } from 'zod';
|
|
4
4
|
interface StepRunOpts<TMemory, I, O> {
|
|
5
5
|
id: string;
|
|
@@ -29,6 +29,22 @@ interface StepToolOpts<I, O> {
|
|
|
29
29
|
tool: Tool<ZodType<I>, ZodType<O>>;
|
|
30
30
|
args?: Partial<I>;
|
|
31
31
|
}
|
|
32
|
+
interface StepSubHarnessOpts<TMemory, O> {
|
|
33
|
+
id: string;
|
|
34
|
+
/** The harness adapter created by a `@noetic-tools/sub-harness-*` factory. Eager or `(ctx) => SubHarness`. */
|
|
35
|
+
harness: Lazy<SubHarness, TMemory>;
|
|
36
|
+
/** Turn prompt. Eager string or `(ctx) => string` getter. */
|
|
37
|
+
prompt: Lazy<string, TMemory>;
|
|
38
|
+
/** Shared harness settings (model, permission mode, …). */
|
|
39
|
+
settings?: SubHarnessSettings;
|
|
40
|
+
/** System instructions applied on the first message of a fresh session. */
|
|
41
|
+
instructions?: Lazy<string | undefined, TMemory>;
|
|
42
|
+
/** Optional Zod schema; when set the assistant text is JSON-parsed and validated. */
|
|
43
|
+
output?: ZodType<O>;
|
|
44
|
+
/** Session reuse + teardown policy across steps. */
|
|
45
|
+
session?: SubHarnessSessionPolicy;
|
|
46
|
+
emit?: boolean | ((eventType: string, data: Record<string, unknown>) => boolean);
|
|
47
|
+
}
|
|
32
48
|
export declare const step: {
|
|
33
49
|
/**
|
|
34
50
|
* Creates a pure async computation step.
|
|
@@ -75,5 +91,39 @@ export declare const step: {
|
|
|
75
91
|
* @throws `NoeticConfigError` with code `MISSING_TOOL` if `tool` is not provided.
|
|
76
92
|
*/
|
|
77
93
|
tool<TMemory = Readonly<Record<string, Record<string, unknown>>>, I = unknown, O = unknown>(opts: StepToolOpts<I, O>): StepTool<TMemory, I, O>;
|
|
94
|
+
/**
|
|
95
|
+
* Creates a step that delegates a turn to the Claude Code harness.
|
|
96
|
+
*
|
|
97
|
+
* @public
|
|
98
|
+
* @param opts.id - Unique step identifier.
|
|
99
|
+
* @param opts.harness - A `claudeCode(...)` adapter from `@noetic-tools/sub-harness-claude-code`.
|
|
100
|
+
* @param opts.prompt - The turn prompt; eager string or `(ctx) => string` getter.
|
|
101
|
+
* @param opts.settings - Shared harness settings (model, permission mode, …).
|
|
102
|
+
* @param opts.output - Optional Zod schema enabling structured output parsing.
|
|
103
|
+
* @returns A `StepSubHarness` of kind `claude-code`, auto-registered in the step registry.
|
|
104
|
+
* @throws `NoeticConfigError` `EMPTY_STEP_ID` / `MISSING_SUB_HARNESS` / `SUB_HARNESS_KIND_MISMATCH`.
|
|
105
|
+
*/
|
|
106
|
+
claudeCode<TMemory = Readonly<Record<string, Record<string, unknown>>>, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TMemory, O>): StepSubHarness<TMemory, I, O>;
|
|
107
|
+
/**
|
|
108
|
+
* Creates a step that delegates a turn to the Codex harness.
|
|
109
|
+
* @public
|
|
110
|
+
* @param opts - See {@link step.claudeCode}; `opts.harness` is a `codex(...)` adapter.
|
|
111
|
+
* @returns A `StepSubHarness` of kind `codex`.
|
|
112
|
+
*/
|
|
113
|
+
codex<TMemory = Readonly<Record<string, Record<string, unknown>>>, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TMemory, O>): StepSubHarness<TMemory, I, O>;
|
|
114
|
+
/**
|
|
115
|
+
* Creates a step that delegates a turn to the opencode harness.
|
|
116
|
+
* @public
|
|
117
|
+
* @param opts - See {@link step.claudeCode}; `opts.harness` is an `opencode(...)` adapter.
|
|
118
|
+
* @returns A `StepSubHarness` of kind `opencode`.
|
|
119
|
+
*/
|
|
120
|
+
opencode<TMemory = Readonly<Record<string, Record<string, unknown>>>, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TMemory, O>): StepSubHarness<TMemory, I, O>;
|
|
121
|
+
/**
|
|
122
|
+
* Creates a step that delegates a turn to the pi harness.
|
|
123
|
+
* @public
|
|
124
|
+
* @param opts - See {@link step.claudeCode}; `opts.harness` is a `pi(...)` adapter.
|
|
125
|
+
* @returns A `StepSubHarness` of kind `pi`.
|
|
126
|
+
*/
|
|
127
|
+
pi<TMemory = Readonly<Record<string, Record<string, unknown>>>, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TMemory, O>): StepSubHarness<TMemory, I, O>;
|
|
78
128
|
};
|
|
79
129
|
export {};
|
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
import { NoeticConfigError } from '@noetic-tools/types';
|
|
2
2
|
import { getDefaultRegistrar } from '../types/step-registrar.js';
|
|
3
3
|
//#endregion
|
|
4
|
+
//#region SubHarness builder helper
|
|
5
|
+
/**
|
|
6
|
+
* Shared construction for every harness step kind. Each `step.<kind>()` is a
|
|
7
|
+
* thin wrapper so the kinds stay individually typed while the validation and
|
|
8
|
+
* registration live in one place.
|
|
9
|
+
*/
|
|
10
|
+
function buildSubHarnessStep(kind, builderName, opts) {
|
|
11
|
+
if (!opts.id || opts.id.trim() === '') {
|
|
12
|
+
throw new NoeticConfigError({
|
|
13
|
+
code: 'EMPTY_STEP_ID',
|
|
14
|
+
message: `${builderName}() requires a non-empty id.`,
|
|
15
|
+
hint: `Pass a unique string as the id field, e.g. ${builderName}({ id: "review", ... }).`,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (!opts.harness) {
|
|
19
|
+
throw new NoeticConfigError({
|
|
20
|
+
code: 'MISSING_SUB_HARNESS',
|
|
21
|
+
message: `${builderName}() requires a harness adapter.`,
|
|
22
|
+
hint: `Pass a harness factory result, e.g. harness: ${builderName.replace('step.', '')}({ model }).`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// Eager adapters are validated now; function-form adapters are validated
|
|
26
|
+
// post-resolution in executeSubHarness so the same SUB_HARNESS_KIND_MISMATCH
|
|
27
|
+
// error surfaces whether the caller passes an adapter or a getter.
|
|
28
|
+
if (typeof opts.harness !== 'function' && opts.harness.harnessId !== kind) {
|
|
29
|
+
throw new NoeticConfigError({
|
|
30
|
+
code: 'SUB_HARNESS_KIND_MISMATCH',
|
|
31
|
+
message: `${builderName}() was given a '${opts.harness.harnessId}' harness.`,
|
|
32
|
+
hint: `Use the matching builder, e.g. step.${opts.harness.harnessId}({ ... }).`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const built = {
|
|
36
|
+
kind,
|
|
37
|
+
...opts,
|
|
38
|
+
};
|
|
39
|
+
getDefaultRegistrar().register(built);
|
|
40
|
+
return built;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
4
43
|
//#region Builders
|
|
5
44
|
export const step = {
|
|
6
45
|
/**
|
|
@@ -114,5 +153,47 @@ export const step = {
|
|
|
114
153
|
getDefaultRegistrar().register(built);
|
|
115
154
|
return built;
|
|
116
155
|
},
|
|
156
|
+
/**
|
|
157
|
+
* Creates a step that delegates a turn to the Claude Code harness.
|
|
158
|
+
*
|
|
159
|
+
* @public
|
|
160
|
+
* @param opts.id - Unique step identifier.
|
|
161
|
+
* @param opts.harness - A `claudeCode(...)` adapter from `@noetic-tools/sub-harness-claude-code`.
|
|
162
|
+
* @param opts.prompt - The turn prompt; eager string or `(ctx) => string` getter.
|
|
163
|
+
* @param opts.settings - Shared harness settings (model, permission mode, …).
|
|
164
|
+
* @param opts.output - Optional Zod schema enabling structured output parsing.
|
|
165
|
+
* @returns A `StepSubHarness` of kind `claude-code`, auto-registered in the step registry.
|
|
166
|
+
* @throws `NoeticConfigError` `EMPTY_STEP_ID` / `MISSING_SUB_HARNESS` / `SUB_HARNESS_KIND_MISMATCH`.
|
|
167
|
+
*/
|
|
168
|
+
claudeCode(opts) {
|
|
169
|
+
return buildSubHarnessStep('claude-code', 'step.claudeCode', opts);
|
|
170
|
+
},
|
|
171
|
+
/**
|
|
172
|
+
* Creates a step that delegates a turn to the Codex harness.
|
|
173
|
+
* @public
|
|
174
|
+
* @param opts - See {@link step.claudeCode}; `opts.harness` is a `codex(...)` adapter.
|
|
175
|
+
* @returns A `StepSubHarness` of kind `codex`.
|
|
176
|
+
*/
|
|
177
|
+
codex(opts) {
|
|
178
|
+
return buildSubHarnessStep('codex', 'step.codex', opts);
|
|
179
|
+
},
|
|
180
|
+
/**
|
|
181
|
+
* Creates a step that delegates a turn to the opencode harness.
|
|
182
|
+
* @public
|
|
183
|
+
* @param opts - See {@link step.claudeCode}; `opts.harness` is an `opencode(...)` adapter.
|
|
184
|
+
* @returns A `StepSubHarness` of kind `opencode`.
|
|
185
|
+
*/
|
|
186
|
+
opencode(opts) {
|
|
187
|
+
return buildSubHarnessStep('opencode', 'step.opencode', opts);
|
|
188
|
+
},
|
|
189
|
+
/**
|
|
190
|
+
* Creates a step that delegates a turn to the pi harness.
|
|
191
|
+
* @public
|
|
192
|
+
* @param opts - See {@link step.claudeCode}; `opts.harness` is a `pi(...)` adapter.
|
|
193
|
+
* @returns A `StepSubHarness` of kind `pi`.
|
|
194
|
+
*/
|
|
195
|
+
pi(opts) {
|
|
196
|
+
return buildSubHarnessStep('pi', 'step.pi', opts);
|
|
197
|
+
},
|
|
117
198
|
};
|
|
118
199
|
//#endregion
|
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
* ones — they register in the step registry, carry retry policies, etc.
|
|
7
7
|
*/
|
|
8
8
|
import type { ContextMemory, MemoryLayer } from '@noetic-tools/memory';
|
|
9
|
-
import type { ExecuteStepFn, Step, Tool } from '@noetic-tools/types';
|
|
9
|
+
import type { ExecuteStepFn, Step, SubHarness, SubHarnessKind, Tool } from '@noetic-tools/types';
|
|
10
10
|
import type { WorkflowDocument, WorkflowNode } from '../schemas/workflow.js';
|
|
11
11
|
/** @public Context required to hydrate a JSON workflow into live steps. */
|
|
12
12
|
export interface HydrationContext {
|
|
13
13
|
tools: ReadonlyMap<string, Tool>;
|
|
14
14
|
executeStep: ExecuteStepFn;
|
|
15
15
|
layers?: ReadonlyMap<string, MemoryLayer>;
|
|
16
|
+
/** SubHarness adapters keyed by harness id, resolving `claude-code`/`codex`/… nodes. */
|
|
17
|
+
subHarnesses?: ReadonlyMap<SubHarnessKind, SubHarness>;
|
|
16
18
|
}
|
|
17
19
|
/**
|
|
18
20
|
* Hydrates a single `WorkflowNode` into a live `Step`.
|
|
@@ -246,6 +246,36 @@ function hydrateEveryNode(node, ctx) {
|
|
|
246
246
|
onError: node.onError,
|
|
247
247
|
});
|
|
248
248
|
}
|
|
249
|
+
const SUB_HARNESS_BUILDERS = {
|
|
250
|
+
'claude-code': (opts) => step.claudeCode(opts),
|
|
251
|
+
codex: (opts) => step.codex(opts),
|
|
252
|
+
opencode: (opts) => step.opencode(opts),
|
|
253
|
+
pi: (opts) => step.pi(opts),
|
|
254
|
+
};
|
|
255
|
+
function hydrateSubHarnessNode(node, ctx) {
|
|
256
|
+
if (node.kind !== 'claude-code' &&
|
|
257
|
+
node.kind !== 'codex' &&
|
|
258
|
+
node.kind !== 'opencode' &&
|
|
259
|
+
node.kind !== 'pi') {
|
|
260
|
+
return frameworkCast(undefined);
|
|
261
|
+
}
|
|
262
|
+
const harness = ctx.subHarnesses?.get(node.kind);
|
|
263
|
+
if (!harness) {
|
|
264
|
+
throw new NoeticConfigError({
|
|
265
|
+
code: 'UNKNOWN_SUB_HARNESS_REFERENCE',
|
|
266
|
+
message: `SubHarness '${node.kind}' referenced in workflow node '${node.id}' is not registered.`,
|
|
267
|
+
hint: `Pass harness adapters via HydrationContext.subHarnesses, e.g. new Map([['${node.kind}', ${node.kind.replace(/-([a-z])/g, (_, c) => c.toUpperCase())}({ model })]]).`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return SUB_HARNESS_BUILDERS[node.kind]({
|
|
271
|
+
id: node.id,
|
|
272
|
+
harness,
|
|
273
|
+
prompt: node.prompt,
|
|
274
|
+
instructions: node.instructions,
|
|
275
|
+
settings: node.settings,
|
|
276
|
+
session: node.session,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
249
279
|
//#endregion
|
|
250
280
|
//#region Handler Registry
|
|
251
281
|
const NODE_HYDRATORS = {
|
|
@@ -258,6 +288,10 @@ const NODE_HYDRATORS = {
|
|
|
258
288
|
loop: hydrateLoopNode,
|
|
259
289
|
sequence: hydrateSequenceNode,
|
|
260
290
|
every: hydrateEveryNode,
|
|
291
|
+
'claude-code': hydrateSubHarnessNode,
|
|
292
|
+
codex: hydrateSubHarnessNode,
|
|
293
|
+
opencode: hydrateSubHarnessNode,
|
|
294
|
+
pi: hydrateSubHarnessNode,
|
|
261
295
|
};
|
|
262
296
|
//#endregion
|
|
263
297
|
//#region Helpers
|
|
@@ -2,7 +2,7 @@ import { ItemSchemaRegistry } from '@noetic-tools/types';
|
|
|
2
2
|
import type { DetachedSpawnOverrides } from './deps/interpreter.js';
|
|
3
3
|
import type { LayerStateStore, RecallCache } from './deps/memory.js';
|
|
4
4
|
import type { CheckpointStore, EventBroadcaster } from './deps/runtime.js';
|
|
5
|
-
import type { AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, ContextMemory, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaExtensions, LLMResponse, LlmProviderConfig, MemoryLayer, ProjectionPolicy, RecallLayerOutput, SessionScope, ShellAdapter, Span, SteeringDecision, Step, StorageAdapter, StreamEvent, StreamingItem, SubprocessAdapter, Tool, TraceExporter, ZodType } from './deps/types.js';
|
|
5
|
+
import type { AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, ContextMemory, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaExtensions, LLMResponse, LlmProviderConfig, MemoryLayer, ProjectionPolicy, RecallLayerOutput, SessionScope, ShellAdapter, Span, SteeringDecision, Step, StorageAdapter, StreamEvent, StreamingItem, SubHarnessSession, SubprocessAdapter, Tool, TraceExporter, ZodType } from './deps/types.js';
|
|
6
6
|
export { createStreamIdleWatchdog } from './model-call.js';
|
|
7
7
|
interface AgentHarnessOpts<TParams extends Record<string, unknown> = Record<string, unknown>> {
|
|
8
8
|
name: string;
|
|
@@ -95,6 +95,13 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
|
|
|
95
95
|
private readonly defaultDeliveryMode;
|
|
96
96
|
private readonly streamIdleTimeoutMs;
|
|
97
97
|
private readonly sessions;
|
|
98
|
+
/**
|
|
99
|
+
* @internal Cross-step harness sessions keyed by `step.session.reuse`, kept
|
|
100
|
+
* alive for the life of this harness so reused coding-agent sessions span
|
|
101
|
+
* multiple steps. Reached by the interpreter's harness handler via
|
|
102
|
+
* `frameworkCast`; do not access from outside core.
|
|
103
|
+
*/
|
|
104
|
+
readonly subHarnessSessions: Map<string, SubHarnessSession>;
|
|
98
105
|
readonly layerStateStore: LayerStateStore;
|
|
99
106
|
/** Per-harness memoization cache for `recallMode: 'eventual'` layers. */
|
|
100
107
|
readonly recallCache: RecallCache;
|
|
@@ -100,6 +100,13 @@ export class AgentHarness {
|
|
|
100
100
|
defaultDeliveryMode;
|
|
101
101
|
streamIdleTimeoutMs;
|
|
102
102
|
sessions = new Map();
|
|
103
|
+
/**
|
|
104
|
+
* @internal Cross-step harness sessions keyed by `step.session.reuse`, kept
|
|
105
|
+
* alive for the life of this harness so reused coding-agent sessions span
|
|
106
|
+
* multiple steps. Reached by the interpreter's harness handler via
|
|
107
|
+
* `frameworkCast`; do not access from outside core.
|
|
108
|
+
*/
|
|
109
|
+
subHarnessSessions = new Map();
|
|
103
110
|
layerStateStore;
|
|
104
111
|
/** Per-harness memoization cache for `recallMode: 'eventual'` layers. */
|
|
105
112
|
recallCache;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { ContextMemory, ExecutionContext, MemoryLayer, ProjectionPolicy, StorageAdapter, } from '@noetic-tools/memory';
|
|
2
|
-
export type { AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaExtensions, LLMResponse, LlmProviderConfig, RecallLayerOutput, SessionScope, ShellAdapter, Span, SteeringDecision, Step, StreamEvent, StreamingItem, SubprocessAdapter, Tool, TraceExporter, } from '@noetic-tools/types';
|
|
2
|
+
export type { AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaExtensions, LLMResponse, LlmProviderConfig, RecallLayerOutput, SessionScope, ShellAdapter, Span, SteeringDecision, Step, StreamEvent, StreamingItem, SubHarnessSession, SubprocessAdapter, Tool, TraceExporter, } from '@noetic-tools/types';
|
|
3
3
|
export { SteeringAction } from '@noetic-tools/types';
|
|
4
4
|
export type { ZodType } from 'zod';
|
package/dist/index.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ export { getRegistry, lookupStep, registerStep } from './runtime/step-registry.j
|
|
|
108
108
|
/** @public */
|
|
109
109
|
export { defaultItemSchemaRegistry, ItemSchema, ItemSchemaRegistry } from '@noetic-tools/types';
|
|
110
110
|
/** @public */
|
|
111
|
-
export type { BranchRoute, BranchWorkflowNode, EveryWorkflowNode, ForkWorkflowNode, LlmWorkflowNode, LoopWorkflowNode, MergeStrategy, ProvideWorkflowNode, SequenceWorkflowNode, SpawnWorkflowNode, ToolWorkflowNode, UntilPredicate, WorkflowDocument, WorkflowNode, } from './schemas/workflow.js';
|
|
111
|
+
export type { BranchRoute, BranchWorkflowNode, EveryWorkflowNode, ForkWorkflowNode, LlmWorkflowNode, LoopWorkflowNode, MergeStrategy, ProvideWorkflowNode, SequenceWorkflowNode, SpawnWorkflowNode, SubHarnessWorkflowNode, ToolWorkflowNode, UntilPredicate, WorkflowDocument, WorkflowNode, } from './schemas/workflow.js';
|
|
112
112
|
/** @public */
|
|
113
113
|
export { MergeStrategySchema, UntilPredicateSchema, validateWorkflow, WorkflowDocumentSchema, WorkflowNodeSchema, walkWorkflow, workflowDepth, } from './schemas/workflow.js';
|
|
114
114
|
/** @public */
|
|
@@ -153,7 +153,11 @@ export type { AfterModelCallParams, AfterModelCallResult, BeforeToolCallParams,
|
|
|
153
153
|
/** @public */
|
|
154
154
|
export { LedgerEntryKind, SteeringAction } from '@noetic-tools/types';
|
|
155
155
|
/** @public */
|
|
156
|
-
export type { ExecuteStepFn, SettleResult, Snapshot, Step, StepBranch, StepFork, StepForkAll, StepForkRace, StepForkSettle, StepLLM, StepLoop, StepProvide, StepRun, StepSpawn, StepTool, Until, Verdict, } from '@noetic-tools/types';
|
|
156
|
+
export type { ExecuteStepFn, SettleResult, Snapshot, Step, StepBranch, StepFork, StepForkAll, StepForkRace, StepForkSettle, StepLLM, StepLoop, StepProvide, StepRun, StepSpawn, StepSubHarness, StepTool, Until, Verdict, } from '@noetic-tools/types';
|
|
157
|
+
/** @public */
|
|
158
|
+
export type { SubHarness, SubHarnessBuiltinTool, SubHarnessContinueState, SubHarnessContinueTurnOptions, SubHarnessFinishReason, SubHarnessPromptTurnOptions, SubHarnessResumeState, SubHarnessRunContext, SubHarnessSession, SubHarnessSessionPolicy, SubHarnessSettings, SubHarnessStartOptions, SubHarnessStreamPart, SubHarnessTurnResult, } from '@noetic-tools/types';
|
|
159
|
+
/** @public */
|
|
160
|
+
export { SUB_HARNESS_KINDS, SubHarnessKind, SubHarnessStreamPartSchema } from '@noetic-tools/types';
|
|
157
161
|
/** @public */
|
|
158
162
|
export type { FsAdapter, FsStats } from '@noetic-tools/types';
|
|
159
163
|
/** @public */
|
package/dist/index.js
CHANGED
|
@@ -108,6 +108,8 @@ export { Slot } from '@noetic-tools/memory';
|
|
|
108
108
|
/** @public */
|
|
109
109
|
export { LedgerEntryKind, SteeringAction } from '@noetic-tools/types';
|
|
110
110
|
/** @public */
|
|
111
|
+
export { SUB_HARNESS_KINDS, SubHarnessKind, SubHarnessStreamPartSchema } from '@noetic-tools/types';
|
|
112
|
+
/** @public */
|
|
111
113
|
export { TIMEOUT_ERROR_PREFIX } from '@noetic-tools/types';
|
|
112
114
|
/** @public Reusable error-serialiser used by custom SubprocessAdapter implementations. */
|
|
113
115
|
export { serializeError } from './adapters/in-memory-subprocess/metadata.js';
|
|
@@ -38,6 +38,12 @@ function walkStep(step, out) {
|
|
|
38
38
|
return;
|
|
39
39
|
case 'run':
|
|
40
40
|
case 'tool':
|
|
41
|
+
// SubHarness steps run their own built-in tools inside the external agent;
|
|
42
|
+
// they contribute nothing to the host unified tool set.
|
|
43
|
+
case 'claude-code':
|
|
44
|
+
case 'codex':
|
|
45
|
+
case 'opencode':
|
|
46
|
+
case 'pi':
|
|
41
47
|
return;
|
|
42
48
|
case 'branch':
|
|
43
49
|
if (step._optimizable) {
|
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
import type { ItemSchemaRegistry, LayerStateStore } from './action-deps.js';
|
|
5
5
|
import type { AgentHarnessContract, Context, ExecuteStepFn, MemoryLayer, StepLLM, StepProvide, StepRun, StepSpawn, StepTool } from './action-types.js';
|
|
6
6
|
export declare function executeRun<TMemory, I, O>(step: StepRun<TMemory, I, O>, input: I, ctx: Context<TMemory>): Promise<O>;
|
|
7
|
+
/**
|
|
8
|
+
* Resolves a `Lazy<T>` step field. If the value is a function, it is invoked
|
|
9
|
+
* with the current context; otherwise the value is returned as-is. Supports
|
|
10
|
+
* both sync and async getters.
|
|
11
|
+
*
|
|
12
|
+
* Lazy fields let a step inspect `ctx.harness.config.params`, `ctx.unifiedTools`,
|
|
13
|
+
* or memory layer state to produce per-execution values (model, instructions,
|
|
14
|
+
* tools) without baking them in at build time.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveLazy<T, TMemory>(value: T | ((ctx: Context<TMemory>) => T | Promise<T>), ctx: Context<TMemory>): Promise<T>;
|
|
7
17
|
export declare function executeLLM<TMemory, I, O>(step: StepLLM<TMemory, I, O>, input: I, ctx: Context<TMemory>, layers?: MemoryLayer[]): Promise<O>;
|
|
8
18
|
export interface ExecuteSpawnOpts {
|
|
9
19
|
layerStore?: LayerStateStore;
|
|
@@ -95,7 +95,7 @@ function mergeRecallResults(base, overrides, slotOf) {
|
|
|
95
95
|
* or memory layer state to produce per-execution values (model, instructions,
|
|
96
96
|
* tools) without baking them in at build time.
|
|
97
97
|
*/
|
|
98
|
-
async function resolveLazy(value, ctx) {
|
|
98
|
+
export async function resolveLazy(value, ctx) {
|
|
99
99
|
if (typeof value !== 'function') {
|
|
100
100
|
return value;
|
|
101
101
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubHarness step handler: drives an external coding-agent harness
|
|
3
|
+
* (Claude Code, Codex, opencode, pi) for one agentic turn and folds its output
|
|
4
|
+
* back into the Noetic execution context — the harness analogue of
|
|
5
|
+
* `executeLLM`.
|
|
6
|
+
*/
|
|
7
|
+
import type { Context, StepSubHarness } from '@noetic-tools/types';
|
|
8
|
+
export declare function executeSubHarness<TMemory, I, O>(step: StepSubHarness<TMemory, I, O>, input: I, ctx: Context<TMemory>): Promise<O>;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubHarness step handler: drives an external coding-agent harness
|
|
3
|
+
* (Claude Code, Codex, opencode, pi) for one agentic turn and folds its output
|
|
4
|
+
* back into the Noetic execution context — the harness analogue of
|
|
5
|
+
* `executeLLM`.
|
|
6
|
+
*/
|
|
7
|
+
import { createMessage, extractAssistantText, frameworkCast, NoeticConfigError, NoeticErrorImpl, } from '@noetic-tools/types';
|
|
8
|
+
import { ZodError } from 'zod';
|
|
9
|
+
import { resolveLazy } from './execute-action.js';
|
|
10
|
+
import { trackUsage } from './message-helpers.js';
|
|
11
|
+
import { SubHarnessEventBridge } from './sub-harness-events.js';
|
|
12
|
+
import { isFunctionCall, isMutableContext } from './typeguards.js';
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region Helpers
|
|
15
|
+
function sessionStore(ctx) {
|
|
16
|
+
return frameworkCast(ctx.harness).subHarnessSessions;
|
|
17
|
+
}
|
|
18
|
+
async function resolveSubHarness(step, ctx) {
|
|
19
|
+
const resolved = await resolveLazy(step.harness, ctx);
|
|
20
|
+
if (!resolved) {
|
|
21
|
+
throw new NoeticConfigError({
|
|
22
|
+
code: 'MISSING_SUB_HARNESS',
|
|
23
|
+
message: `step.${step.kind}(${JSON.stringify(step.id)}) resolved no harness adapter.`,
|
|
24
|
+
hint: 'Pass a harness factory result, e.g. harness: claudeCode({ model }).',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (resolved.harnessId !== step.kind) {
|
|
28
|
+
throw new NoeticConfigError({
|
|
29
|
+
code: 'SUB_HARNESS_KIND_MISMATCH',
|
|
30
|
+
message: `step.${step.kind}(${JSON.stringify(step.id)}) was given a '${resolved.harnessId}' harness.`,
|
|
31
|
+
hint: `Use the matching builder for this adapter, e.g. step.${resolved.harnessId}({ ... }).`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
function resolveTurnText(resolvedPrompt, input) {
|
|
37
|
+
if (resolvedPrompt && resolvedPrompt.length > 0) {
|
|
38
|
+
return resolvedPrompt;
|
|
39
|
+
}
|
|
40
|
+
return typeof input === 'string' ? input : '';
|
|
41
|
+
}
|
|
42
|
+
function buildRunContext(ctx) {
|
|
43
|
+
return {
|
|
44
|
+
cwd: ctx.cwdState.cwd,
|
|
45
|
+
fs: ctx.fs,
|
|
46
|
+
shell: ctx.shell,
|
|
47
|
+
subprocess: ctx.subprocess,
|
|
48
|
+
threadId: ctx.threadId,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function toLlmResponse(result) {
|
|
52
|
+
const usage = result.usage;
|
|
53
|
+
return {
|
|
54
|
+
items: result.items,
|
|
55
|
+
usage: {
|
|
56
|
+
inputTokens: usage?.input ?? 0,
|
|
57
|
+
outputTokens: usage?.output ?? 0,
|
|
58
|
+
cachedTokens: usage?.cached,
|
|
59
|
+
},
|
|
60
|
+
cost: result.cost,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function applyTurnResult(ctx, result) {
|
|
64
|
+
const toolCalls = [];
|
|
65
|
+
for (const item of result.items) {
|
|
66
|
+
ctx.itemLog.append(item);
|
|
67
|
+
if (isFunctionCall(item)) {
|
|
68
|
+
toolCalls.push(item);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const llmResponse = toLlmResponse(result);
|
|
72
|
+
trackUsage(ctx, llmResponse);
|
|
73
|
+
const meta = {
|
|
74
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
75
|
+
usage: result.usage ? llmResponse.usage : undefined,
|
|
76
|
+
cost: result.cost,
|
|
77
|
+
responseItems: result.items,
|
|
78
|
+
};
|
|
79
|
+
if (isMutableContext(ctx)) {
|
|
80
|
+
ctx.lastStepMeta = meta;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function teardownSession(session, mode) {
|
|
84
|
+
if (mode === 'detach' && session.doDetach) {
|
|
85
|
+
await session.doDetach();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (mode === 'destroy' && session.doDestroy) {
|
|
89
|
+
await session.doDestroy();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await session.doStop();
|
|
93
|
+
}
|
|
94
|
+
async function startOrReuseSession(step, harness, ctx, baseCtx, history) {
|
|
95
|
+
const reuseKey = step.session?.reuse;
|
|
96
|
+
const store = sessionStore(baseCtx);
|
|
97
|
+
if (reuseKey) {
|
|
98
|
+
const existing = store.get(reuseKey);
|
|
99
|
+
if (existing) {
|
|
100
|
+
return {
|
|
101
|
+
session: existing,
|
|
102
|
+
reuseKey,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const session = await harness.doStart({
|
|
107
|
+
settings: step.settings,
|
|
108
|
+
instructions: await resolveLazy(step.instructions, ctx),
|
|
109
|
+
history,
|
|
110
|
+
ctx: buildRunContext(baseCtx),
|
|
111
|
+
});
|
|
112
|
+
if (reuseKey) {
|
|
113
|
+
store.set(reuseKey, session);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
session,
|
|
117
|
+
reuseKey,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Tear the session down per policy after a successful turn. Reused sessions
|
|
122
|
+
* stay alive by default; an explicit `onComplete` overrides that.
|
|
123
|
+
*/
|
|
124
|
+
async function finalizeSession(resolution, policy, baseCtx) {
|
|
125
|
+
const { session, reuseKey } = resolution;
|
|
126
|
+
if (!reuseKey) {
|
|
127
|
+
await teardownSession(session, policy?.onComplete ?? 'stop');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const mode = policy?.onComplete;
|
|
131
|
+
if (mode === 'stop' || mode === 'destroy') {
|
|
132
|
+
await teardownSession(session, mode);
|
|
133
|
+
sessionStore(baseCtx).delete(reuseKey);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (mode === 'detach') {
|
|
137
|
+
await teardownSession(session, 'detach');
|
|
138
|
+
}
|
|
139
|
+
// Default for a reused session: keep it alive in the store.
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region Public API
|
|
143
|
+
export async function executeSubHarness(step, input, ctx) {
|
|
144
|
+
const baseCtx = frameworkCast(ctx);
|
|
145
|
+
const harness = await resolveSubHarness(step, ctx);
|
|
146
|
+
const resolvedPrompt = await resolveLazy(step.prompt, ctx);
|
|
147
|
+
const turnText = resolveTurnText(resolvedPrompt, input);
|
|
148
|
+
if (turnText.trim() === '') {
|
|
149
|
+
throw new NoeticConfigError({
|
|
150
|
+
code: 'MISSING_PROMPT',
|
|
151
|
+
message: `step.${step.kind}(${JSON.stringify(step.id)}) resolved an empty prompt.`,
|
|
152
|
+
hint: 'Provide a non-empty `prompt`, or pass a string input to the step.',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// Capture the conversation so far (from earlier LLM/sub-harness steps and
|
|
156
|
+
// turns) BEFORE appending this turn's prompt, so a fresh session is seeded
|
|
157
|
+
// with full context of the conversation.
|
|
158
|
+
const priorHistory = [
|
|
159
|
+
...baseCtx.itemLog.items,
|
|
160
|
+
];
|
|
161
|
+
baseCtx.itemLog.append(createMessage(turnText, 'user'));
|
|
162
|
+
const resolution = await startOrReuseSession(step, harness, ctx, baseCtx, priorHistory);
|
|
163
|
+
const bridge = new SubHarnessEventBridge(step, baseCtx);
|
|
164
|
+
bridge.begin();
|
|
165
|
+
let result;
|
|
166
|
+
try {
|
|
167
|
+
result = await resolution.session.doPromptTurn({
|
|
168
|
+
prompt: turnText,
|
|
169
|
+
emit: (part) => bridge.forward(part),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
// Best-effort teardown of a fresh session before surfacing the failure;
|
|
174
|
+
// reused sessions are left intact for a later step to retry against.
|
|
175
|
+
if (!resolution.reuseKey) {
|
|
176
|
+
await teardownSession(resolution.session, 'destroy').catch(() => undefined);
|
|
177
|
+
}
|
|
178
|
+
if (e instanceof NoeticErrorImpl) {
|
|
179
|
+
throw e;
|
|
180
|
+
}
|
|
181
|
+
throw new NoeticErrorImpl({
|
|
182
|
+
kind: 'step_failed',
|
|
183
|
+
stepId: step.id,
|
|
184
|
+
cause: e instanceof Error ? e : new Error(String(e)),
|
|
185
|
+
retriesExhausted: false,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
bridge.finalize(result);
|
|
189
|
+
applyTurnResult(baseCtx, result);
|
|
190
|
+
await finalizeSession(resolution, step.session, baseCtx);
|
|
191
|
+
const lastText = result.text.length > 0 ? result.text : extractAssistantText(result.items);
|
|
192
|
+
if (step.output) {
|
|
193
|
+
try {
|
|
194
|
+
const parsed = JSON.parse(lastText);
|
|
195
|
+
return step.output.parse(parsed);
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
if (e instanceof SyntaxError || e instanceof ZodError) {
|
|
199
|
+
throw new NoeticErrorImpl({
|
|
200
|
+
kind: 'llm_parse_error',
|
|
201
|
+
stepId: step.id,
|
|
202
|
+
raw: lastText,
|
|
203
|
+
schema: step.output,
|
|
204
|
+
zodError: e instanceof ZodError
|
|
205
|
+
? e
|
|
206
|
+
: new ZodError([
|
|
207
|
+
{
|
|
208
|
+
code: 'custom',
|
|
209
|
+
message: `Invalid JSON: ${e.message}`,
|
|
210
|
+
path: [],
|
|
211
|
+
},
|
|
212
|
+
]),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
throw e;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return frameworkCast(lastText);
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
@@ -4,6 +4,7 @@ import { ContextImpl } from '../runtime/context-impl.js';
|
|
|
4
4
|
import { DetachedHandleImpl } from '../runtime/detached-handle.js';
|
|
5
5
|
import { executeLLM, executeProvide, executeRun, executeSpawn, executeTool, } from './execute-action.js';
|
|
6
6
|
import { executeBranch, executeEvery, executeFork, executeLoop } from './execute-control.js';
|
|
7
|
+
import { executeSubHarness } from './execute-sub-harness.js';
|
|
7
8
|
import { isMutableContext } from './typeguards.js';
|
|
8
9
|
//#region Constants
|
|
9
10
|
const MAX_DEPTH = 64;
|
|
@@ -29,6 +30,20 @@ function buildSpawnOpts(ctx) {
|
|
|
29
30
|
layerStore: harnessWithLayerStore(baseCtx).layerStateStore,
|
|
30
31
|
};
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the per-step framework-event emit option. Both `llm` and the harness
|
|
35
|
+
* step kinds carry an `emit` field; every other kind defaults to enabled.
|
|
36
|
+
*/
|
|
37
|
+
function resolveStepEmit(step) {
|
|
38
|
+
if (step.kind === 'llm' ||
|
|
39
|
+
step.kind === 'claude-code' ||
|
|
40
|
+
step.kind === 'codex' ||
|
|
41
|
+
step.kind === 'opencode' ||
|
|
42
|
+
step.kind === 'pi') {
|
|
43
|
+
return step.emit;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
32
47
|
/**
|
|
33
48
|
* Resolve the subprocess adapter that should handle dispatch for this step.
|
|
34
49
|
* Precedence is `step.subprocess ?? ctx.harness.subprocess`. Per-call
|
|
@@ -138,7 +153,7 @@ export async function execute(step, input, ctx) {
|
|
|
138
153
|
stepId: step.id,
|
|
139
154
|
kind: step.kind,
|
|
140
155
|
};
|
|
141
|
-
const emit = step
|
|
156
|
+
const emit = resolveStepEmit(step);
|
|
142
157
|
if (shouldEmit(emit, 'step_started', startedData)) {
|
|
143
158
|
emitFrameworkEvent({
|
|
144
159
|
broadcaster,
|
|
@@ -166,6 +181,12 @@ export async function execute(step, input, ctx) {
|
|
|
166
181
|
case 'llm':
|
|
167
182
|
result = await executeLLM(step, input, ctx, baseCtx.layers);
|
|
168
183
|
break;
|
|
184
|
+
case 'claude-code':
|
|
185
|
+
case 'codex':
|
|
186
|
+
case 'opencode':
|
|
187
|
+
case 'pi':
|
|
188
|
+
result = await executeSubHarness(step, input, ctx);
|
|
189
|
+
break;
|
|
169
190
|
case 'tool':
|
|
170
191
|
result = await executeTool(step, input, ctx, baseCtx.harness);
|
|
171
192
|
break;
|