@noetic-tools/core 5.4.0 → 5.4.1
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/acp-agent-tool.d.ts +67 -0
- package/dist/builders/acp-agent-tool.js +87 -0
- package/dist/builders/dynamic-workflow.js +2 -2
- package/dist/builders/step-builders.d.ts +42 -42
- package/dist/builders/step-builders.js +38 -62
- package/dist/builders/workflow-hydrator.d.ts +8 -4
- package/dist/builders/workflow-hydrator.js +16 -24
- package/dist/builders/workflow-step.d.ts +3 -3
- package/dist/builders/workflow-step.js +1 -1
- package/dist/harness/agent-harness.d.ts +36 -7
- package/dist/harness/agent-harness.js +49 -6
- package/dist/harness/deps/types.d.ts +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +3 -3
- package/dist/interpreter/acp-events.d.ts +67 -0
- package/dist/interpreter/acp-events.js +256 -0
- package/dist/interpreter/acp-history.d.ts +22 -0
- package/dist/interpreter/acp-history.js +58 -0
- package/dist/interpreter/collect-tools.js +3 -6
- package/dist/interpreter/execute-acp-agent.d.ts +10 -0
- package/dist/interpreter/execute-acp-agent.js +505 -0
- package/dist/interpreter/execute.js +4 -11
- package/dist/interpreter/structured-output.d.ts +1 -1
- package/dist/interpreter/structured-output.js +1 -1
- package/dist/portable/builders.d.ts +2 -0
- package/dist/portable/builders.js +3 -1
- package/dist/runtime/acp-session-store.d.ts +75 -0
- package/dist/runtime/acp-session-store.js +0 -0
- package/dist/schemas/workflow.d.ts +1 -1
- package/package.json +3 -3
- package/schema/noetic-workflow.schema.json +215 -249
- package/dist/interpreter/execute-sub-harness.d.ts +0 -8
- package/dist/interpreter/execute-sub-harness.js +0 -222
- package/dist/interpreter/sub-harness-events.d.ts +0 -48
- package/dist/interpreter/sub-harness-events.js +0 -208
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { frameworkCast, ItemSchemaRegistry, NoeticConfigError } from '@noetic-tools/types';
|
|
2
2
|
import { SpanImpl } from '../observability/span-impl.js';
|
|
3
3
|
import { NoopExporter } from '../observability/trace-exporter.js';
|
|
4
|
+
import { AcpSessionStore } from '../runtime/acp-session-store.js';
|
|
4
5
|
import { createInMemoryFsAdapter, createInMemoryShellAdapter, createInMemorySubprocessAdapter, OpenRouter, } from './deps/adapters.js';
|
|
5
6
|
import { afterModelCallLayers, allocateBudgets, assembleView, beforeToolCallLayers, completeLayers, contextToExecCtx, createContextCacheStore, createLayerStateStore, createRecallCache, DEFAULT_PROJECTION, disposeLayers, executeRerender, initLayers, projectHistoryLayers, recallLayers, recallLayersAtomic, recallLayersEventual, resolveLayerTools, runAppendPipeline, storeLayers, } from './deps/context.js';
|
|
6
7
|
import { collectAllTools, deduplicateTools, dispatchStepThroughAdapter, execute, prepareBandedView, } from './deps/interpreter.js';
|
|
@@ -192,12 +193,53 @@ export class AgentHarness {
|
|
|
192
193
|
defaultDeliveryMode;
|
|
193
194
|
sessions = new Map();
|
|
194
195
|
/**
|
|
195
|
-
* @internal
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* `frameworkCast`; do not access from outside core.
|
|
196
|
+
* @internal Live ACP connections. Reached by the interpreter via
|
|
197
|
+
* `frameworkCast`; use {@link listAcpSessions}, {@link getAcpSession},
|
|
198
|
+
* {@link cancelAcpSession}, and {@link closeAcpSessions} from outside core.
|
|
199
199
|
*/
|
|
200
|
-
|
|
200
|
+
acpSessions = new AcpSessionStore();
|
|
201
|
+
/**
|
|
202
|
+
* Snapshot of every live ACP sub-agent, for a UI that wants to show what is
|
|
203
|
+
* running and let a user act on it. Read-only: a session's turns are driven
|
|
204
|
+
* by steps, so nothing here can start work behind the runtime's back.
|
|
205
|
+
* @public
|
|
206
|
+
*/
|
|
207
|
+
listAcpSessions() {
|
|
208
|
+
return this.acpSessions.list();
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The live connection + session behind a handle from {@link listAcpSessions}
|
|
212
|
+
* (or a step's `session.reuse` key), or `undefined` when nothing matches.
|
|
213
|
+
* @public
|
|
214
|
+
*/
|
|
215
|
+
getAcpSession(key) {
|
|
216
|
+
return this.acpSessions.get(key);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Interrupt whatever the sub-agent under `key` is doing, via `session/cancel`.
|
|
220
|
+
* The connection stays open and the turn still returns — with the `cancelled`
|
|
221
|
+
* stop reason, which the running step surfaces as a `cancelled` error.
|
|
222
|
+
* Returns false when nothing matches the key.
|
|
223
|
+
* @public
|
|
224
|
+
*/
|
|
225
|
+
async cancelAcpSession(key) {
|
|
226
|
+
const entry = this.acpSessions.get(key);
|
|
227
|
+
if (!entry) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
await entry.session.cancel();
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Close every ACP connection this harness still holds, whatever its
|
|
235
|
+
* keep-alive scope. A connection owns a live agent — usually a child process
|
|
236
|
+
* whose stdio keeps the event loop alive — so a `keepAlive: 'harness'`
|
|
237
|
+
* session must be closed by its owner or the host will not exit. Idempotent.
|
|
238
|
+
* @public
|
|
239
|
+
*/
|
|
240
|
+
async closeAcpSessions() {
|
|
241
|
+
await this.acpSessions.closeAll();
|
|
242
|
+
}
|
|
201
243
|
layerStateStore;
|
|
202
244
|
/** Per-harness memoization cache for `recallMode: 'eventual'` layers. */
|
|
203
245
|
recallCache;
|
|
@@ -520,6 +562,7 @@ export class AgentHarness {
|
|
|
520
562
|
else {
|
|
521
563
|
this.rootRunDepth.delete(ctx.id);
|
|
522
564
|
this.channelStore.closeExecution(ctx.id);
|
|
565
|
+
await this.acpSessions.closeOwnedBy(ctx.id);
|
|
523
566
|
}
|
|
524
567
|
}
|
|
525
568
|
}
|
|
@@ -848,7 +891,7 @@ export class AgentHarness {
|
|
|
848
891
|
*
|
|
849
892
|
* Aborting a context rejects everything blocked on it (channel `recv`
|
|
850
893
|
* waiters, parked senders) with `{ kind: 'cancelled' }`, stops its in-flight
|
|
851
|
-
* model call and
|
|
894
|
+
* model call and ACP agent turn, and makes the next step boundary throw
|
|
852
895
|
* `cancelled`.
|
|
853
896
|
*
|
|
854
897
|
* Cancellation is cooperative: a layer hook already in flight is allowed to
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { ContextCacheConfig, ContextCacheStore, ContextData, ContextLayer, ExecutionContext, ProjectionPolicy, StorageAdapter, } from '@noetic-tools/context';
|
|
2
|
-
export type { AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaConfig, ItemSchemaExtensions, LLMResponse, LlmProviderConfig, RecallLayerOutput, SessionScope, SessionUsage, ShellAdapter, Span, SteeringDecision, Step, StreamEvent, StreamingItem,
|
|
2
|
+
export type { AcpKeepAlive, AcpLiveSession, AcpSessionInfo, AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaConfig, ItemSchemaExtensions, LLMResponse, LlmProviderConfig, RecallLayerOutput, SessionScope, SessionUsage, ShellAdapter, Span, SteeringDecision, Step, StreamEvent, StreamingItem, 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
|
@@ -7,6 +7,10 @@ export { createInMemorySubprocessAdapter } from './adapters/in-memory-subprocess
|
|
|
7
7
|
/** @public */
|
|
8
8
|
export { createOpenRouterEmbed } from './adapters/openrouter.js';
|
|
9
9
|
/** @public */
|
|
10
|
+
export type { AcpAgentToolOptions } from './builders/acp-agent-tool.js';
|
|
11
|
+
/** @public */
|
|
12
|
+
export { acpAgentTool } from './builders/acp-agent-tool.js';
|
|
13
|
+
/** @public */
|
|
10
14
|
export { channel } from './builders/channel-builder.js';
|
|
11
15
|
/** @public */
|
|
12
16
|
export { context } from './builders/context-builder.js';
|
|
@@ -100,7 +104,7 @@ export { getRegistry, lookupStep, registerStep } from './runtime/step-registry.j
|
|
|
100
104
|
/** @public */
|
|
101
105
|
export { defaultItemSchemaRegistry, ItemSchema, ItemSchemaRegistry } from '@noetic-tools/types';
|
|
102
106
|
/** @public */
|
|
103
|
-
export type { CallModelWorkflowNode, ConditionalRoute, ConditionalWorkflowNode, InParallelWorkflowNode, InvokeToolWorkflowNode, LoopWorkflowNode, MergeStrategy, ScheduleWorkflowNode, SequenceWorkflowNode, SpawnWorkflowNode, SubflowWorkflowNode,
|
|
107
|
+
export type { AcpAgentWorkflowNode, CallModelWorkflowNode, ConditionalRoute, ConditionalWorkflowNode, InParallelWorkflowNode, InvokeToolWorkflowNode, LoopWorkflowNode, MergeStrategy, ScheduleWorkflowNode, SequenceWorkflowNode, SpawnWorkflowNode, SubflowWorkflowNode, UntilPredicate, WithContextWorkflowNode, WorkflowDocument, WorkflowNode, } from './schemas/workflow.js';
|
|
104
108
|
/** @public */
|
|
105
109
|
export { MergeStrategySchema, UntilPredicateSchema, validateWorkflow, WorkflowDocumentSchema, WorkflowNodeSchema, walkWorkflow, workflowDepth, } from './schemas/workflow.js';
|
|
106
110
|
/** @public */
|
|
@@ -147,11 +151,11 @@ export type { AfterModelCallParams, AfterModelCallResult, BeforeToolCallParams,
|
|
|
147
151
|
/** @public */
|
|
148
152
|
export { LedgerEntryKind, SteeringAction } from '@noetic-tools/types';
|
|
149
153
|
/** @public */
|
|
150
|
-
export type { ExecuteStepFn, SettleResult, Snapshot, Step, StepCallModel, StepConditional, StepInParallel, StepInParallelAll, StepInParallelRace, StepInParallelSettle, StepInvokeTool, StepLoop, StepRunCode, StepSpawn,
|
|
154
|
+
export type { ExecuteStepFn, SettleResult, Snapshot, Step, StepAcpAgent, StepCallModel, StepConditional, StepInParallel, StepInParallelAll, StepInParallelRace, StepInParallelSettle, StepInvokeTool, StepLoop, StepRunCode, StepSpawn, StepWithContext, Until, Verdict, } from '@noetic-tools/types';
|
|
151
155
|
/** @public */
|
|
152
|
-
export type {
|
|
156
|
+
export type { AcpAgent, AcpAgentCapabilities, AcpAgentConnection, AcpAuthMethod, AcpAvailableCommand, AcpBoundPermissionHandler, AcpClientCapabilityConfig, AcpClientHost, AcpConnectOptions, AcpContentBlock, AcpKeepAlive, AcpLiveSession, AcpLoadSessionOptions, AcpMcpServer, AcpNewSessionOptions, AcpPermissionHandler, AcpPermissionOption, AcpPermissionOutcome, AcpPermissionPolicy, AcpPermissionRequestInfo, AcpPermissionRule, AcpPermissionSteerer, AcpPlanEntry, AcpPromptCapabilities, AcpPromptOptions, AcpRequestPermissionRequest, AcpSession, AcpSessionDisposer, AcpSessionInfo, AcpSessionMode, AcpSessionModeState, AcpSessionNotification, AcpSessionPolicy, AcpStopReason, AcpToolCallContent, AcpToolCallStatus, AcpToolKind, AcpTransport, AcpTransportFactory, AcpTransportOptions, AcpTurnResult, } from '@noetic-tools/types';
|
|
153
157
|
/** @public */
|
|
154
|
-
export {
|
|
158
|
+
export { ACP_AGENT_STEP_KIND, AcpCapabilityError, AcpConnectError, AcpPermissionDecision, isAcpCapabilityError, isAcpConnectError, } from '@noetic-tools/types';
|
|
155
159
|
/** @public */
|
|
156
160
|
export type { FsAdapter, FsStats } from '@noetic-tools/types';
|
|
157
161
|
/** @public */
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ export { createInMemoryShellAdapter } from './adapters/in-memory-shell-adapter.j
|
|
|
7
7
|
export { createInMemorySubprocessAdapter } from './adapters/in-memory-subprocess-adapter.js';
|
|
8
8
|
/** @public */
|
|
9
9
|
export { createOpenRouterEmbed } from './adapters/openrouter.js';
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/** @public */
|
|
11
|
+
export { acpAgentTool } from './builders/acp-agent-tool.js';
|
|
12
12
|
/** @public */
|
|
13
13
|
export { channel } from './builders/channel-builder.js';
|
|
14
14
|
/** @public */
|
|
@@ -100,7 +100,7 @@ export { Slot } from '@noetic-tools/context';
|
|
|
100
100
|
/** @public */
|
|
101
101
|
export { LedgerEntryKind, SteeringAction } from '@noetic-tools/types';
|
|
102
102
|
/** @public */
|
|
103
|
-
export {
|
|
103
|
+
export { ACP_AGENT_STEP_KIND, AcpCapabilityError, AcpConnectError, AcpPermissionDecision, isAcpCapabilityError, isAcpConnectError, } from '@noetic-tools/types';
|
|
104
104
|
/** @public */
|
|
105
105
|
export { TIMEOUT_ERROR_PREFIX } from '@noetic-tools/types';
|
|
106
106
|
/** @public Reusable error-serialiser used by custom SubprocessAdapter implementations. */
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps an ACP turn's `session/update` notifications onto the harness's
|
|
3
|
+
* observable event surface.
|
|
4
|
+
*
|
|
5
|
+
* Every one of the protocol's eight update variants is translated into the same
|
|
6
|
+
* `source: 'sdk'` broadcaster events the model-call path emits, so a coding
|
|
7
|
+
* agent's text, reasoning, and tool calls flow through `getTextStream()` /
|
|
8
|
+
* `getReasoningStream()` / `getItemStream()` / `getFullStream()` exactly like an
|
|
9
|
+
* LLM step's output. Each notification is *also* re-emitted raw as an
|
|
10
|
+
* `acp_event` framework event for consumers that want the protocol payload.
|
|
11
|
+
*
|
|
12
|
+
* `finalize()` guarantees a turn always emits its output: if an agent returns a
|
|
13
|
+
* stop reason without streaming any text or tool calls, the result's content is
|
|
14
|
+
* synthesized into events.
|
|
15
|
+
*/
|
|
16
|
+
import type { AcpClientActivity, AcpSessionNotification, AcpTurnResult, Context, ContextData, StepAcpAgent } from '@noetic-tools/types';
|
|
17
|
+
export declare class AcpEventBridge {
|
|
18
|
+
private readonly broadcaster?;
|
|
19
|
+
private readonly agentName;
|
|
20
|
+
private readonly stepId;
|
|
21
|
+
private readonly agentId;
|
|
22
|
+
private readonly emitOption;
|
|
23
|
+
private started;
|
|
24
|
+
private completed;
|
|
25
|
+
private nextOutputIndex;
|
|
26
|
+
private messageOutputIndex;
|
|
27
|
+
private sawText;
|
|
28
|
+
/** Tool call ids already announced, so a `tool_call_update` never re-opens one. */
|
|
29
|
+
private readonly announcedToolCalls;
|
|
30
|
+
constructor(step: Pick<StepAcpAgent, 'id' | 'emit'>, agentId: string, ctx: Context<ContextData>);
|
|
31
|
+
/**
|
|
32
|
+
* Open the turn on the event surface. Emits `response.created` so every turn
|
|
33
|
+
* brackets with a lifecycle marker — even an agent that streams nothing.
|
|
34
|
+
*/
|
|
35
|
+
begin(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Record one client-side operation the agent performed. Unlike a `tool_call`
|
|
38
|
+
* update — which is the agent's own account of what it did — this is what it
|
|
39
|
+
* actually asked us to do, refusals included.
|
|
40
|
+
*/
|
|
41
|
+
forwardActivity(activity: AcpClientActivity): void;
|
|
42
|
+
/** Forward one notification: a structured framework event + the mapped sdk events. */
|
|
43
|
+
forward(notification: AcpSessionNotification): void;
|
|
44
|
+
/**
|
|
45
|
+
* Ensure the turn's output reached the event surface. For an agent that
|
|
46
|
+
* returns a result without streaming, synthesize events from the result.
|
|
47
|
+
*/
|
|
48
|
+
finalize(result: AcpTurnResult): void;
|
|
49
|
+
/**
|
|
50
|
+
* Close a turn that failed. The bracket a consumer waits on must be honoured
|
|
51
|
+
* even when the turn dies mid-stream, so an open message is closed and the
|
|
52
|
+
* response completes with an error rather than the stream simply stopping.
|
|
53
|
+
*/
|
|
54
|
+
abort(cause: unknown): void;
|
|
55
|
+
/**
|
|
56
|
+
* Emit one framework event under this step's envelope. The `sdk` sibling
|
|
57
|
+
* below does the same job for the model-stream surface; both gate on
|
|
58
|
+
* `emitOption` so `emit: false` silences every surface at once.
|
|
59
|
+
*/
|
|
60
|
+
private framework;
|
|
61
|
+
private sdk;
|
|
62
|
+
private start;
|
|
63
|
+
private openMessage;
|
|
64
|
+
private closeMessage;
|
|
65
|
+
private emitToolCall;
|
|
66
|
+
private translate;
|
|
67
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps an ACP turn's `session/update` notifications onto the harness's
|
|
3
|
+
* observable event surface.
|
|
4
|
+
*
|
|
5
|
+
* Every one of the protocol's eight update variants is translated into the same
|
|
6
|
+
* `source: 'sdk'` broadcaster events the model-call path emits, so a coding
|
|
7
|
+
* agent's text, reasoning, and tool calls flow through `getTextStream()` /
|
|
8
|
+
* `getReasoningStream()` / `getItemStream()` / `getFullStream()` exactly like an
|
|
9
|
+
* LLM step's output. Each notification is *also* re-emitted raw as an
|
|
10
|
+
* `acp_event` framework event for consumers that want the protocol payload.
|
|
11
|
+
*
|
|
12
|
+
* `finalize()` guarantees a turn always emits its output: if an agent returns a
|
|
13
|
+
* stop reason without streaming any text or tool calls, the result's content is
|
|
14
|
+
* synthesized into events.
|
|
15
|
+
*/
|
|
16
|
+
import { emitFrameworkEvent, getBroadcaster, shouldEmit } from '../runtime/broadcaster-utils.js';
|
|
17
|
+
import { isFunctionCall } from './typeguards.js';
|
|
18
|
+
/** Flatten an ACP content block for the text event surface. */
|
|
19
|
+
function blockText(block) {
|
|
20
|
+
if (block.type === 'text') {
|
|
21
|
+
return block.text;
|
|
22
|
+
}
|
|
23
|
+
if (block.type === 'resource_link') {
|
|
24
|
+
return `[resource: ${block.uri}]`;
|
|
25
|
+
}
|
|
26
|
+
if (block.type === 'resource') {
|
|
27
|
+
return 'text' in block.resource ? block.resource.text : `[resource: ${block.resource.uri}]`;
|
|
28
|
+
}
|
|
29
|
+
return `[${block.type}: ${block.mimeType}]`;
|
|
30
|
+
}
|
|
31
|
+
export class AcpEventBridge {
|
|
32
|
+
broadcaster;
|
|
33
|
+
agentName;
|
|
34
|
+
stepId;
|
|
35
|
+
agentId;
|
|
36
|
+
emitOption;
|
|
37
|
+
started = false;
|
|
38
|
+
completed = false;
|
|
39
|
+
nextOutputIndex = 0;
|
|
40
|
+
messageOutputIndex = null;
|
|
41
|
+
sawText = false;
|
|
42
|
+
/** Tool call ids already announced, so a `tool_call_update` never re-opens one. */
|
|
43
|
+
announcedToolCalls = new Set();
|
|
44
|
+
constructor(step, agentId, ctx) {
|
|
45
|
+
this.broadcaster = getBroadcaster(ctx);
|
|
46
|
+
this.agentName = ctx.harness.config.name;
|
|
47
|
+
this.stepId = step.id;
|
|
48
|
+
this.agentId = agentId;
|
|
49
|
+
this.emitOption = step.emit;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Open the turn on the event surface. Emits `response.created` so every turn
|
|
53
|
+
* brackets with a lifecycle marker — even an agent that streams nothing.
|
|
54
|
+
*/
|
|
55
|
+
begin() {
|
|
56
|
+
this.start();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Record one client-side operation the agent performed. Unlike a `tool_call`
|
|
60
|
+
* update — which is the agent's own account of what it did — this is what it
|
|
61
|
+
* actually asked us to do, refusals included.
|
|
62
|
+
*/
|
|
63
|
+
forwardActivity(activity) {
|
|
64
|
+
this.framework('acp_client_activity', {
|
|
65
|
+
...activity,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Forward one notification: a structured framework event + the mapped sdk events. */
|
|
69
|
+
forward(notification) {
|
|
70
|
+
if (!this.broadcaster) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.framework('acp_event', {
|
|
74
|
+
sessionId: notification.sessionId,
|
|
75
|
+
update: notification.update,
|
|
76
|
+
});
|
|
77
|
+
this.translate(notification.update);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Ensure the turn's output reached the event surface. For an agent that
|
|
81
|
+
* returns a result without streaming, synthesize events from the result.
|
|
82
|
+
*/
|
|
83
|
+
finalize(result) {
|
|
84
|
+
if (!this.broadcaster) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!this.sawText && result.text.length > 0) {
|
|
88
|
+
this.openMessage();
|
|
89
|
+
this.sdk('response.output_text.delta', {
|
|
90
|
+
delta: result.text,
|
|
91
|
+
}, this.messageOutputIndex ?? 0);
|
|
92
|
+
this.sawText = true;
|
|
93
|
+
}
|
|
94
|
+
// Close the message before synthesizing tool calls, so output items finish
|
|
95
|
+
// in index order. Emitting them first left item 0 closing after items 1..n,
|
|
96
|
+
// which reads as interleaved output to a consumer tracking `outputIndex`.
|
|
97
|
+
this.closeMessage();
|
|
98
|
+
for (const item of result.items) {
|
|
99
|
+
if (isFunctionCall(item) && !this.announcedToolCalls.has(item.callId)) {
|
|
100
|
+
this.emitToolCall(item.callId, item.name, item.arguments);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!this.completed) {
|
|
104
|
+
this.start();
|
|
105
|
+
this.sdk('response.completed', {
|
|
106
|
+
stopReason: result.stopReason,
|
|
107
|
+
});
|
|
108
|
+
this.completed = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Close a turn that failed. The bracket a consumer waits on must be honoured
|
|
113
|
+
* even when the turn dies mid-stream, so an open message is closed and the
|
|
114
|
+
* response completes with an error rather than the stream simply stopping.
|
|
115
|
+
*/
|
|
116
|
+
abort(cause) {
|
|
117
|
+
if (!this.broadcaster || this.completed) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.closeMessage();
|
|
121
|
+
this.start();
|
|
122
|
+
this.sdk('response.completed', {
|
|
123
|
+
stopReason: 'error',
|
|
124
|
+
error: cause instanceof Error ? cause.message : String(cause),
|
|
125
|
+
});
|
|
126
|
+
this.completed = true;
|
|
127
|
+
}
|
|
128
|
+
//#region internals
|
|
129
|
+
/**
|
|
130
|
+
* Emit one framework event under this step's envelope. The `sdk` sibling
|
|
131
|
+
* below does the same job for the model-stream surface; both gate on
|
|
132
|
+
* `emitOption` so `emit: false` silences every surface at once.
|
|
133
|
+
*/
|
|
134
|
+
framework(eventType, payload) {
|
|
135
|
+
if (!this.broadcaster) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const data = {
|
|
139
|
+
stepId: this.stepId,
|
|
140
|
+
agentId: this.agentId,
|
|
141
|
+
...payload,
|
|
142
|
+
};
|
|
143
|
+
if (!shouldEmit(this.emitOption, eventType, data)) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
emitFrameworkEvent({
|
|
147
|
+
broadcaster: this.broadcaster,
|
|
148
|
+
agentName: this.agentName,
|
|
149
|
+
eventType,
|
|
150
|
+
data,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
sdk(type, data, outputIndex) {
|
|
154
|
+
if (!this.broadcaster) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!shouldEmit(this.emitOption, type, data)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.broadcaster.emit({
|
|
161
|
+
source: 'sdk',
|
|
162
|
+
type,
|
|
163
|
+
data,
|
|
164
|
+
outputIndex,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
start() {
|
|
168
|
+
if (this.started) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.started = true;
|
|
172
|
+
this.sdk('response.created', {});
|
|
173
|
+
}
|
|
174
|
+
openMessage() {
|
|
175
|
+
if (this.messageOutputIndex !== null) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.start();
|
|
179
|
+
const id = `acp-msg-${crypto.randomUUID()}`;
|
|
180
|
+
this.messageOutputIndex = this.nextOutputIndex++;
|
|
181
|
+
this.sdk('response.output_item.added', {
|
|
182
|
+
item: {
|
|
183
|
+
id,
|
|
184
|
+
type: 'message',
|
|
185
|
+
role: 'assistant',
|
|
186
|
+
status: 'in_progress',
|
|
187
|
+
content: [
|
|
188
|
+
{
|
|
189
|
+
type: 'output_text',
|
|
190
|
+
text: '',
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
}, this.messageOutputIndex);
|
|
195
|
+
}
|
|
196
|
+
closeMessage() {
|
|
197
|
+
if (this.messageOutputIndex === null) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const index = this.messageOutputIndex;
|
|
201
|
+
this.sdk('response.output_text.done', {}, index);
|
|
202
|
+
this.sdk('response.output_item.done', {}, index);
|
|
203
|
+
this.messageOutputIndex = null;
|
|
204
|
+
}
|
|
205
|
+
emitToolCall(toolCallId, name, input) {
|
|
206
|
+
this.start();
|
|
207
|
+
const index = this.nextOutputIndex++;
|
|
208
|
+
const args = typeof input === 'string' ? input : JSON.stringify(input ?? {});
|
|
209
|
+
this.sdk('response.output_item.added', {
|
|
210
|
+
item: {
|
|
211
|
+
id: toolCallId,
|
|
212
|
+
type: 'function_call',
|
|
213
|
+
status: 'in_progress',
|
|
214
|
+
callId: toolCallId,
|
|
215
|
+
name,
|
|
216
|
+
arguments: '',
|
|
217
|
+
},
|
|
218
|
+
}, index);
|
|
219
|
+
this.sdk('response.function_call_arguments.delta', {
|
|
220
|
+
delta: args,
|
|
221
|
+
}, index);
|
|
222
|
+
this.sdk('response.function_call_arguments.done', {
|
|
223
|
+
arguments: args,
|
|
224
|
+
}, index);
|
|
225
|
+
this.sdk('response.output_item.done', {}, index);
|
|
226
|
+
this.announcedToolCalls.add(toolCallId);
|
|
227
|
+
}
|
|
228
|
+
translate(update) {
|
|
229
|
+
if (update.sessionUpdate === 'agent_message_chunk') {
|
|
230
|
+
this.openMessage();
|
|
231
|
+
this.sdk('response.output_text.delta', {
|
|
232
|
+
delta: blockText(update.content),
|
|
233
|
+
}, this.messageOutputIndex ?? 0);
|
|
234
|
+
this.sawText = true;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (update.sessionUpdate === 'agent_thought_chunk') {
|
|
238
|
+
this.start();
|
|
239
|
+
this.sdk('response.reasoning.delta', {
|
|
240
|
+
delta: blockText(update.content),
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (update.sessionUpdate === 'tool_call') {
|
|
245
|
+
this.closeMessage();
|
|
246
|
+
this.emitToolCall(update.toolCallId, update.title, update.rawInput);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// `user_message_chunk` (replayed history), `tool_call_update`, `plan`,
|
|
250
|
+
// `available_commands_update`, and `current_mode_update` have no model-stream
|
|
251
|
+
// analogue; they surface on the full stream under their protocol names.
|
|
252
|
+
this.sdk(`acp.${update.sessionUpdate}`, {
|
|
253
|
+
update,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gives an ACP agent the prior conversation.
|
|
3
|
+
*
|
|
4
|
+
* ACP sessions own their own history on the agent side, but a freshly opened
|
|
5
|
+
* session knows nothing about the Noetic steps that ran before it. Seeding the
|
|
6
|
+
* first prompt with a transcript of the conversation so far means a coding agent
|
|
7
|
+
* running after a chain of `callModel` steps understands what was already
|
|
8
|
+
* established instead of answering out of context.
|
|
9
|
+
*/
|
|
10
|
+
import type { Item } from '@noetic-tools/types';
|
|
11
|
+
/** @public Render conversation history as a plain-text transcript. */
|
|
12
|
+
export declare function formatConversation(items: ReadonlyArray<Item>): string;
|
|
13
|
+
/**
|
|
14
|
+
* Fold the conversation so far into a turn's prompt. Returns the prompt
|
|
15
|
+
* unchanged when there is no history, so the very first step of a run is not
|
|
16
|
+
* padded with an empty preamble.
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export declare function withHistoryPrompt(input: {
|
|
20
|
+
prompt: string;
|
|
21
|
+
history: ReadonlyArray<Item>;
|
|
22
|
+
}): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gives an ACP agent the prior conversation.
|
|
3
|
+
*
|
|
4
|
+
* ACP sessions own their own history on the agent side, but a freshly opened
|
|
5
|
+
* session knows nothing about the Noetic steps that ran before it. Seeding the
|
|
6
|
+
* first prompt with a transcript of the conversation so far means a coding agent
|
|
7
|
+
* running after a chain of `callModel` steps understands what was already
|
|
8
|
+
* established instead of answering out of context.
|
|
9
|
+
*/
|
|
10
|
+
function roleLabel(role) {
|
|
11
|
+
if (role === 'assistant') {
|
|
12
|
+
return 'Assistant';
|
|
13
|
+
}
|
|
14
|
+
if (role === 'user') {
|
|
15
|
+
return 'User';
|
|
16
|
+
}
|
|
17
|
+
return 'System';
|
|
18
|
+
}
|
|
19
|
+
function messageText(item) {
|
|
20
|
+
if (item.type !== 'message') {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const text = item.content
|
|
24
|
+
.map((part) => ('text' in part && typeof part.text === 'string' ? part.text : ''))
|
|
25
|
+
.join('');
|
|
26
|
+
return text.length > 0 ? `${roleLabel(item.role)}: ${text}` : null;
|
|
27
|
+
}
|
|
28
|
+
/** @public Render conversation history as a plain-text transcript. */
|
|
29
|
+
export function formatConversation(items) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
for (const item of items) {
|
|
32
|
+
const line = messageText(item);
|
|
33
|
+
if (line) {
|
|
34
|
+
lines.push(line);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (item.type === 'function_call') {
|
|
38
|
+
lines.push(`Assistant called ${item.name}(${item.arguments})`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Fold the conversation so far into a turn's prompt. Returns the prompt
|
|
45
|
+
* unchanged when there is no history, so the very first step of a run is not
|
|
46
|
+
* padded with an empty preamble.
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
export function withHistoryPrompt(input) {
|
|
50
|
+
if (input.history.length === 0) {
|
|
51
|
+
return input.prompt;
|
|
52
|
+
}
|
|
53
|
+
const transcript = formatConversation(input.history);
|
|
54
|
+
if (transcript.length === 0) {
|
|
55
|
+
return input.prompt;
|
|
56
|
+
}
|
|
57
|
+
return `Conversation so far:\n${transcript}\n\nCurrent request:\n${input.prompt}`;
|
|
58
|
+
}
|
|
@@ -43,12 +43,9 @@ function walkStep(step, out) {
|
|
|
43
43
|
return;
|
|
44
44
|
case 'runCode':
|
|
45
45
|
case 'invokeTool':
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
case '
|
|
49
|
-
case 'codex':
|
|
50
|
-
case 'opencode':
|
|
51
|
-
case 'pi':
|
|
46
|
+
// An ACP agent runs its own tools inside the external agent process; it
|
|
47
|
+
// contributes nothing to the host unified tool set.
|
|
48
|
+
case 'acp-agent':
|
|
52
49
|
return;
|
|
53
50
|
case 'conditional':
|
|
54
51
|
if (step._optimizable) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP step handler: drives an external coding agent over the Agent Client
|
|
3
|
+
* Protocol for one prompt turn and folds its output back into the Noetic
|
|
4
|
+
* execution context — the ACP analogue of `executeCallModel`.
|
|
5
|
+
*
|
|
6
|
+
* Core reaches the agent only through the `@noetic-tools/types` contract, so no
|
|
7
|
+
* protocol library or agent SDK enters its dependency graph.
|
|
8
|
+
*/
|
|
9
|
+
import type { Context, ContextLayer, StepAcpAgent } from '@noetic-tools/types';
|
|
10
|
+
export declare function executeAcpAgent<TContext, I, O>(step: StepAcpAgent<TContext, I, O>, input: I, ctx: Context<TContext>, layers?: ContextLayer[]): Promise<O>;
|