@nextclaw/ncp-toolkit 0.1.0 → 0.2.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
@@ -5,10 +5,14 @@ Toolkit implementations built on top of `@nextclaw/ncp` protocol contracts.
5
5
  ## Build
6
6
 
7
7
  ```bash
8
- pnpm -C packages/nextclaw-ncp-toolkit build
8
+ pnpm -C packages/ncp-packages/nextclaw-ncp-toolkit build
9
9
  ```
10
10
 
11
11
  ## Scope
12
12
 
13
13
  - Reference conversation-state manager implementations
14
14
  - Protocol-level helper logic that depends on `@nextclaw/ncp` contracts
15
+ - Composable agent backend building block: `DefaultNcpAgentBackend`
16
+ - Default in-memory adapter: `InMemoryAgentSessionStore`
17
+ - In-process adapter helper: `createAgentClientFromServer`
18
+ - Runtime throwable helper: `NcpErrorException`
package/dist/index.d.ts CHANGED
@@ -1,22 +1,22 @@
1
- import { NcpAgentConversationStateManager, NcpConversationSnapshot, NcpEndpointEvent, NcpRequestEnvelope, NcpMessageSentPayload, NcpMessageAcceptedPayload, NcpResponseEnvelope, NcpCompletedEnvelope, NcpFailedEnvelope, NcpMessageAbortPayload, NcpTextStartPayload, NcpTextDeltaPayload, NcpTextEndPayload, NcpReasoningStartPayload, NcpReasoningDeltaPayload, NcpReasoningEndPayload, NcpToolCallStartPayload, NcpToolCallArgsPayload, NcpToolCallArgsDeltaPayload, NcpToolCallEndPayload, NcpToolCallResultPayload, NcpRunStartedPayload, NcpRunFinishedPayload, NcpRunErrorPayload, NcpRunMetadataPayload, NcpError } from '@nextclaw/ncp';
1
+ import { NcpAgentConversationStateManager, NcpAgentConversationSnapshot, NcpAgentConversationHydrationParams, NcpEndpointEvent, NcpMessageSentPayload, NcpMessageAbortPayload, NcpTextStartPayload, NcpTextDeltaPayload, NcpTextEndPayload, NcpReasoningStartPayload, NcpReasoningDeltaPayload, NcpReasoningEndPayload, NcpToolCallStartPayload, NcpToolCallArgsPayload, NcpToolCallArgsDeltaPayload, NcpToolCallEndPayload, NcpToolCallResultPayload, NcpRunStartedPayload, NcpRunFinishedPayload, NcpRunErrorPayload, NcpRunMetadataPayload, NcpError, NcpAgentServerEndpoint, NcpAgentClientEndpoint, NcpEndpointSubscriber, NcpAgentRuntime, NcpRequestEnvelope, NcpMessage, NcpSessionApi, NcpAgentStreamProvider, NcpAgentRunApi, NcpEndpointManifest, NcpAgentRunSendOptions, NcpStreamRequestPayload, NcpAgentRunStreamOptions, NcpSessionSummary, NcpErrorCode } from '@nextclaw/ncp';
2
2
 
3
3
  declare class DefaultNcpAgentConversationStateManager implements NcpAgentConversationStateManager {
4
4
  private messages;
5
5
  private streamingMessage;
6
6
  private error;
7
+ private activeRun;
7
8
  private readonly listeners;
8
9
  private readonly toolCallMessageIdByCallId;
9
10
  private readonly toolCallArgsRawByCallId;
11
+ private snapshotCache;
12
+ private snapshotVersion;
10
13
  private stateVersion;
11
- getSnapshot(): NcpConversationSnapshot;
12
- subscribe(listener: (snapshot: NcpConversationSnapshot) => void): () => boolean;
13
- dispatch(event: NcpEndpointEvent): void;
14
- handleMessageRequest(payload: NcpRequestEnvelope): void;
14
+ getSnapshot(): NcpAgentConversationSnapshot;
15
+ subscribe(listener: (snapshot: NcpAgentConversationSnapshot) => void): () => boolean;
16
+ reset(): void;
17
+ hydrate(payload: NcpAgentConversationHydrationParams): void;
18
+ dispatch(event: NcpEndpointEvent): Promise<void>;
15
19
  handleMessageSent(payload: NcpMessageSentPayload): void;
16
- handleMessageAccepted(_payload: NcpMessageAcceptedPayload): void;
17
- handleMessageIncoming(payload: NcpResponseEnvelope): void;
18
- handleMessageCompleted(payload: NcpCompletedEnvelope): void;
19
- handleMessageFailed(payload: NcpFailedEnvelope): void;
20
20
  handleMessageAbort(payload: NcpMessageAbortPayload): void;
21
21
  handleMessageTextStart(payload: NcpTextStartPayload): void;
22
22
  handleMessageTextDelta(payload: NcpTextDeltaPayload): void;
@@ -29,10 +29,10 @@ declare class DefaultNcpAgentConversationStateManager implements NcpAgentConvers
29
29
  handleMessageToolCallArgsDelta(payload: NcpToolCallArgsDeltaPayload): void;
30
30
  handleMessageToolCallEnd(payload: NcpToolCallEndPayload): void;
31
31
  handleMessageToolCallResult(payload: NcpToolCallResultPayload): void;
32
- handleRunStarted(_payload: NcpRunStartedPayload): void;
32
+ handleRunStarted(payload: NcpRunStartedPayload): void;
33
33
  handleRunFinished(_payload: NcpRunFinishedPayload): void;
34
34
  handleRunError(payload: NcpRunErrorPayload): void;
35
- handleRunMetadata(_payload: NcpRunMetadataPayload): void;
35
+ handleRunMetadata(payload: NcpRunMetadataPayload): void;
36
36
  handleEndpointError(payload: NcpError): void;
37
37
  private applyToolCallArgs;
38
38
  private ensureStreamingMessage;
@@ -41,11 +41,127 @@ declare class DefaultNcpAgentConversationStateManager implements NcpAgentConvers
41
41
  private findToolInvocationPart;
42
42
  private findToolNameByCallId;
43
43
  private upsertToolInvocationPart;
44
- private findLastReasoningPartIndex;
45
44
  private upsertMessage;
46
45
  private replaceStreamingMessage;
47
46
  private setError;
47
+ private clearActiveRun;
48
48
  private clearToolCallTrackingByMessageId;
49
+ private notifyListeners;
49
50
  }
50
51
 
51
- export { DefaultNcpAgentConversationStateManager };
52
+ /**
53
+ * Creates an NcpAgentClientEndpoint that forwards to an in-process NcpAgentServerEndpoint.
54
+ * Use when the agent runs in-process and you need to pass a client endpoint to the HTTP server.
55
+ */
56
+ declare function createAgentClientFromServer(server: NcpAgentServerEndpoint): NcpAgentClientEndpoint;
57
+
58
+ declare class EventPublisher {
59
+ private readonly listeners;
60
+ private readonly closeListeners;
61
+ private closed;
62
+ subscribe(listener: NcpEndpointSubscriber): () => void;
63
+ onClose(listener: () => void): () => void;
64
+ publish(event: NcpEndpointEvent): void;
65
+ close(): void;
66
+ }
67
+
68
+ type RuntimeFactoryParams = {
69
+ sessionId: string;
70
+ stateManager: NcpAgentConversationStateManager;
71
+ };
72
+ type CreateRuntimeFn = (params: RuntimeFactoryParams) => NcpAgentRuntime;
73
+ type AgentSessionRecord = {
74
+ sessionId: string;
75
+ messages: NcpMessage[];
76
+ updatedAt: string;
77
+ };
78
+ type LiveSessionExecution = {
79
+ controller: AbortController;
80
+ publisher: EventPublisher;
81
+ requestEnvelope: NcpRequestEnvelope;
82
+ abortHandled: boolean;
83
+ closed: boolean;
84
+ };
85
+ type LiveSessionState = {
86
+ sessionId: string;
87
+ runtime: NcpAgentRuntime;
88
+ stateManager: NcpAgentConversationStateManager;
89
+ activeExecution: LiveSessionExecution | null;
90
+ };
91
+ interface AgentSessionStore {
92
+ getSession(sessionId: string): Promise<AgentSessionRecord | null>;
93
+ listSessions(): Promise<AgentSessionRecord[]>;
94
+ saveSession(session: AgentSessionRecord): Promise<void>;
95
+ deleteSession(sessionId: string): Promise<AgentSessionRecord | null>;
96
+ }
97
+
98
+ type DefaultNcpAgentBackendConfig = {
99
+ createRuntime: CreateRuntimeFn;
100
+ sessionStore: AgentSessionStore;
101
+ endpointId?: string;
102
+ version?: string;
103
+ metadata?: Record<string, unknown>;
104
+ supportedPartTypes?: NcpEndpointManifest["supportedPartTypes"];
105
+ expectedLatency?: NcpEndpointManifest["expectedLatency"];
106
+ };
107
+ declare class DefaultNcpAgentBackend implements NcpAgentServerEndpoint, NcpSessionApi, NcpAgentStreamProvider, NcpAgentRunApi {
108
+ readonly manifest: NcpEndpointManifest & {
109
+ endpointKind: "agent";
110
+ };
111
+ private readonly sessionStore;
112
+ private readonly sessionRegistry;
113
+ private readonly executor;
114
+ private readonly publisher;
115
+ private started;
116
+ constructor(config: DefaultNcpAgentBackendConfig);
117
+ start(): Promise<void>;
118
+ stop(): Promise<void>;
119
+ emit(event: NcpEndpointEvent): Promise<void>;
120
+ subscribe(listener: (event: NcpEndpointEvent) => void): () => void;
121
+ send(envelope: NcpRequestEnvelope, options?: NcpAgentRunSendOptions): AsyncIterable<NcpEndpointEvent>;
122
+ abort(payload: NcpMessageAbortPayload): Promise<void>;
123
+ stream(payloadOrParams: NcpStreamRequestPayload | {
124
+ payload: NcpStreamRequestPayload;
125
+ signal: AbortSignal;
126
+ }, opts?: NcpAgentRunStreamOptions): AsyncIterable<NcpEndpointEvent>;
127
+ listSessions(): Promise<NcpSessionSummary[]>;
128
+ listSessionMessages(sessionId: string): Promise<NcpMessage[]>;
129
+ getSession(sessionId: string): Promise<NcpSessionSummary | null>;
130
+ deleteSession(sessionId: string): Promise<void>;
131
+ private ensureStarted;
132
+ private startSessionExecution;
133
+ private finishSessionExecution;
134
+ private closeExecution;
135
+ private publishLiveEvent;
136
+ private handleRequest;
137
+ private streamToSubscribers;
138
+ private handleAbort;
139
+ private createAbortEvent;
140
+ private persistSession;
141
+ }
142
+
143
+ declare class AgentRunExecutor {
144
+ private readonly persistSession;
145
+ constructor(persistSession: (sessionId: string) => Promise<void>);
146
+ executeRun(session: LiveSessionState, envelope: NcpRequestEnvelope, controller: AbortController): AsyncGenerator<NcpEndpointEvent>;
147
+ private publishFailure;
148
+ }
149
+
150
+ declare class InMemoryAgentSessionStore implements AgentSessionStore {
151
+ private readonly sessions;
152
+ getSession(sessionId: string): Promise<AgentSessionRecord | null>;
153
+ listSessions(): Promise<AgentSessionRecord[]>;
154
+ saveSession(session: AgentSessionRecord): Promise<void>;
155
+ deleteSession(sessionId: string): Promise<AgentSessionRecord | null>;
156
+ }
157
+
158
+ /**
159
+ * Throwable form of protocol error for exception-based control flows.
160
+ */
161
+ declare class NcpErrorException extends Error {
162
+ readonly code: NcpErrorCode;
163
+ readonly details?: Record<string, unknown>;
164
+ constructor(code: NcpErrorCode, message: string, details?: Record<string, unknown>);
165
+ }
166
+
167
+ export { AgentRunExecutor, type AgentSessionRecord, type AgentSessionStore, type CreateRuntimeFn, DefaultNcpAgentBackend, type DefaultNcpAgentBackendConfig, DefaultNcpAgentConversationStateManager, EventPublisher, InMemoryAgentSessionStore, type LiveSessionExecution, type LiveSessionState, NcpErrorException, type RuntimeFactoryParams, createAgentClientFromServer };
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  // src/agent/agent-conversation-state-manager.ts
2
+ import {
3
+ NcpEventType
4
+ } from "@nextclaw/ncp";
2
5
  var DEFAULT_ASSISTANT_ROLE = "assistant";
3
6
  var cloneMessage = (message) => {
4
7
  return {
@@ -13,7 +16,7 @@ var buildRuntimeError = (payload) => {
13
16
  code: "runtime-error",
14
17
  message: message && message.length > 0 ? message : "Agent run failed.",
15
18
  details: {
16
- sessionKey: payload.sessionKey,
19
+ sessionId: payload.sessionId,
17
20
  messageId: payload.messageId,
18
21
  threadId: payload.threadId,
19
22
  runId: payload.runId
@@ -24,205 +27,153 @@ var DefaultNcpAgentConversationStateManager = class {
24
27
  messages = [];
25
28
  streamingMessage = null;
26
29
  error = null;
30
+ activeRun = null;
27
31
  listeners = /* @__PURE__ */ new Set();
28
32
  toolCallMessageIdByCallId = /* @__PURE__ */ new Map();
29
33
  toolCallArgsRawByCallId = /* @__PURE__ */ new Map();
34
+ snapshotCache = null;
35
+ snapshotVersion = -1;
30
36
  stateVersion = 0;
31
37
  getSnapshot() {
32
- return {
38
+ if (this.snapshotCache && this.snapshotVersion === this.stateVersion) {
39
+ return this.snapshotCache;
40
+ }
41
+ const snapshot = {
33
42
  messages: this.messages.map((message) => cloneMessage(message)),
34
43
  streamingMessage: this.streamingMessage ? cloneMessage(this.streamingMessage) : null,
35
- error: this.error ? { ...this.error, details: this.error.details ? { ...this.error.details } : void 0 } : null
44
+ error: this.error ? { ...this.error, details: this.error.details ? { ...this.error.details } : void 0 } : null,
45
+ activeRun: this.activeRun ? { ...this.activeRun } : null
36
46
  };
47
+ this.snapshotCache = snapshot;
48
+ this.snapshotVersion = this.stateVersion;
49
+ return snapshot;
37
50
  }
38
51
  subscribe(listener) {
39
52
  this.listeners.add(listener);
40
53
  return () => this.listeners.delete(listener);
41
54
  }
42
- dispatch(event) {
55
+ reset() {
56
+ if (this.messages.length === 0 && !this.streamingMessage && !this.error && !this.activeRun && this.toolCallMessageIdByCallId.size === 0 && this.toolCallArgsRawByCallId.size === 0) {
57
+ return;
58
+ }
59
+ this.messages = [];
60
+ this.streamingMessage = null;
61
+ this.error = null;
62
+ this.activeRun = null;
63
+ this.toolCallMessageIdByCallId.clear();
64
+ this.toolCallArgsRawByCallId.clear();
65
+ this.stateVersion += 1;
66
+ this.notifyListeners();
67
+ }
68
+ hydrate(payload) {
69
+ this.messages = payload.messages.map((message) => cloneMessage(message));
70
+ this.streamingMessage = null;
71
+ this.error = null;
72
+ this.activeRun = payload.activeRun ? {
73
+ ...payload.activeRun,
74
+ sessionId: payload.activeRun.sessionId ?? payload.sessionId,
75
+ abortDisabledReason: payload.activeRun.abortDisabledReason ?? null
76
+ } : null;
77
+ this.toolCallMessageIdByCallId.clear();
78
+ this.toolCallArgsRawByCallId.clear();
79
+ this.stateVersion += 1;
80
+ this.notifyListeners();
81
+ }
82
+ async dispatch(event) {
43
83
  const versionBeforeDispatch = this.stateVersion;
44
84
  switch (event.type) {
45
- case "message.request":
46
- this.handleMessageRequest(event.payload);
47
- break;
48
- case "message.sent":
85
+ case NcpEventType.MessageSent:
49
86
  this.handleMessageSent(event.payload);
50
87
  break;
51
- case "message.accepted":
52
- this.handleMessageAccepted(event.payload);
53
- break;
54
- case "message.incoming":
55
- this.handleMessageIncoming(event.payload);
56
- break;
57
- case "message.completed":
58
- this.handleMessageCompleted(event.payload);
59
- break;
60
- case "message.failed":
61
- this.handleMessageFailed(event.payload);
62
- break;
63
- case "message.abort":
88
+ case NcpEventType.MessageAbort:
64
89
  this.handleMessageAbort(event.payload);
65
90
  break;
66
- case "message.text-start":
91
+ case NcpEventType.MessageTextStart:
67
92
  this.handleMessageTextStart(event.payload);
68
93
  break;
69
- case "message.text-delta":
94
+ case NcpEventType.MessageTextDelta:
70
95
  this.handleMessageTextDelta(event.payload);
71
96
  break;
72
- case "message.text-end":
97
+ case NcpEventType.MessageTextEnd:
73
98
  this.handleMessageTextEnd(event.payload);
74
99
  break;
75
- case "message.reasoning-start":
100
+ case NcpEventType.MessageReasoningStart:
76
101
  this.handleMessageReasoningStart(event.payload);
77
102
  break;
78
- case "message.reasoning-delta":
103
+ case NcpEventType.MessageReasoningDelta:
79
104
  this.handleMessageReasoningDelta(event.payload);
80
105
  break;
81
- case "message.reasoning-end":
106
+ case NcpEventType.MessageReasoningEnd:
82
107
  this.handleMessageReasoningEnd(event.payload);
83
108
  break;
84
- case "message.tool-call-start":
109
+ case NcpEventType.MessageToolCallStart:
85
110
  this.handleMessageToolCallStart(event.payload);
86
111
  break;
87
- case "message.tool-call-args":
112
+ case NcpEventType.MessageToolCallArgs:
88
113
  this.handleMessageToolCallArgs(event.payload);
89
114
  break;
90
- case "message.tool-call-args-delta":
115
+ case NcpEventType.MessageToolCallArgsDelta:
91
116
  this.handleMessageToolCallArgsDelta(event.payload);
92
117
  break;
93
- case "message.tool-call-end":
118
+ case NcpEventType.MessageToolCallEnd:
94
119
  this.handleMessageToolCallEnd(event.payload);
95
120
  break;
96
- case "message.tool-call-result":
121
+ case NcpEventType.MessageToolCallResult:
97
122
  this.handleMessageToolCallResult(event.payload);
98
123
  break;
99
- case "run.started":
124
+ case NcpEventType.RunStarted:
100
125
  this.handleRunStarted(event.payload);
101
126
  break;
102
- case "run.finished":
127
+ case NcpEventType.RunFinished:
103
128
  this.handleRunFinished(event.payload);
104
129
  break;
105
- case "run.error":
130
+ case NcpEventType.RunError:
106
131
  this.handleRunError(event.payload);
107
132
  break;
108
- case "run.metadata":
133
+ case NcpEventType.RunMetadata:
109
134
  this.handleRunMetadata(event.payload);
110
135
  break;
111
- case "endpoint.error":
136
+ case NcpEventType.EndpointError:
112
137
  this.handleEndpointError(event.payload);
113
138
  break;
114
- case "endpoint.ready":
115
- case "typing.start":
116
- case "typing.end":
117
- case "presence.updated":
118
- case "message.read":
119
- case "message.delivered":
120
- case "message.recalled":
121
- case "message.reaction":
122
- break;
123
139
  default:
124
140
  break;
125
141
  }
126
142
  if (this.stateVersion !== versionBeforeDispatch) {
127
- const snapshot = this.getSnapshot();
128
- for (const listener of this.listeners) {
129
- listener(snapshot);
130
- }
143
+ this.notifyListeners();
131
144
  }
132
145
  }
133
- handleMessageRequest(payload) {
134
- this.upsertMessage(payload.message);
135
- this.setError(null);
136
- }
137
146
  handleMessageSent(payload) {
138
147
  this.upsertMessage(payload.message);
139
148
  this.setError(null);
140
149
  }
141
- handleMessageAccepted(_payload) {
142
- }
143
- handleMessageIncoming(payload) {
144
- const incomingMessage = cloneMessage(payload.message);
145
- if (incomingMessage.status === "streaming" || incomingMessage.status === "pending") {
146
- this.replaceStreamingMessage(incomingMessage);
147
- this.setError(null);
148
- return;
149
- }
150
- this.upsertMessage(incomingMessage);
151
- if (this.streamingMessage?.id === incomingMessage.id) {
152
- this.replaceStreamingMessage(null);
153
- this.clearToolCallTrackingByMessageId(incomingMessage.id);
154
- }
155
- this.setError(null);
156
- }
157
- handleMessageCompleted(payload) {
158
- const completedMessage = {
159
- ...cloneMessage(payload.message),
160
- status: "final"
161
- };
162
- this.upsertMessage(completedMessage);
163
- if (this.streamingMessage?.id === completedMessage.id) {
164
- this.replaceStreamingMessage(null);
165
- }
166
- this.clearToolCallTrackingByMessageId(completedMessage.id);
167
- this.setError(null);
168
- }
169
- handleMessageFailed(payload) {
170
- this.setError(payload.error);
171
- const targetMessageId = payload.messageId?.trim();
172
- if (!targetMessageId) {
173
- return;
174
- }
175
- if (this.streamingMessage?.id === targetMessageId) {
176
- this.upsertMessage({
177
- ...this.streamingMessage,
178
- status: "error"
179
- });
180
- this.replaceStreamingMessage(null);
181
- this.clearToolCallTrackingByMessageId(targetMessageId);
182
- return;
183
- }
184
- const messageIndex = this.messages.findIndex((message) => message.id === targetMessageId);
185
- if (messageIndex < 0) {
186
- return;
187
- }
188
- const nextMessages = [...this.messages];
189
- nextMessages[messageIndex] = {
190
- ...nextMessages[messageIndex],
191
- status: "error"
192
- };
193
- this.messages = nextMessages;
194
- this.stateVersion += 1;
195
- }
196
150
  handleMessageAbort(payload) {
197
151
  const targetMessageId = payload.messageId?.trim();
198
- this.setError({
199
- code: "abort-error",
200
- message: "Message aborted.",
201
- details: {
202
- messageId: targetMessageId,
203
- correlationId: payload.correlationId
204
- }
205
- });
152
+ this.clearActiveRun();
153
+ this.setError(null);
206
154
  if (this.streamingMessage && (!targetMessageId || this.streamingMessage.id === targetMessageId)) {
155
+ const streamingMessageId = this.streamingMessage.id;
207
156
  this.upsertMessage({
208
157
  ...this.streamingMessage,
209
- status: "error"
158
+ status: "final"
210
159
  });
211
160
  this.replaceStreamingMessage(null);
212
161
  if (targetMessageId) {
213
162
  this.clearToolCallTrackingByMessageId(targetMessageId);
163
+ } else {
164
+ this.clearToolCallTrackingByMessageId(streamingMessageId);
214
165
  }
215
166
  }
216
167
  }
217
168
  handleMessageTextStart(payload) {
218
- this.ensureStreamingMessage(payload.sessionKey, payload.messageId, "streaming");
169
+ this.ensureStreamingMessage(payload.sessionId, payload.messageId, "streaming");
219
170
  this.setError(null);
220
171
  }
221
172
  handleMessageTextDelta(payload) {
222
173
  if (!payload.delta) {
223
174
  return;
224
175
  }
225
- const targetMessage = this.ensureStreamingMessage(payload.sessionKey, payload.messageId, "streaming");
176
+ const targetMessage = this.ensureStreamingMessage(payload.sessionId, payload.messageId, "streaming");
226
177
  const nextParts = [...targetMessage.parts];
227
178
  const lastPart = nextParts[nextParts.length - 1];
228
179
  if (lastPart?.type === "text") {
@@ -252,23 +203,20 @@ var DefaultNcpAgentConversationStateManager = class {
252
203
  });
253
204
  }
254
205
  handleMessageReasoningStart(payload) {
255
- this.ensureStreamingMessage(payload.sessionKey, payload.messageId, "streaming");
206
+ this.ensureStreamingMessage(payload.sessionId, payload.messageId, "streaming");
256
207
  }
257
208
  handleMessageReasoningDelta(payload) {
258
209
  if (!payload.delta) {
259
210
  return;
260
211
  }
261
- const targetMessage = this.ensureStreamingMessage(payload.sessionKey, payload.messageId, "streaming");
212
+ const targetMessage = this.ensureStreamingMessage(payload.sessionId, payload.messageId, "streaming");
262
213
  const nextParts = [...targetMessage.parts];
263
- const reasoningPartIndex = this.findLastReasoningPartIndex(nextParts);
264
- if (reasoningPartIndex >= 0) {
265
- const existingPart = nextParts[reasoningPartIndex];
266
- if (existingPart?.type === "reasoning") {
267
- nextParts[reasoningPartIndex] = {
268
- type: "reasoning",
269
- text: `${existingPart.text}${payload.delta}`
270
- };
271
- }
214
+ const lastPart = nextParts[nextParts.length - 1];
215
+ if (lastPart?.type === "reasoning") {
216
+ nextParts[nextParts.length - 1] = {
217
+ type: "reasoning",
218
+ text: `${lastPart.text}${payload.delta}`
219
+ };
272
220
  } else {
273
221
  nextParts.push({ type: "reasoning", text: payload.delta });
274
222
  }
@@ -285,7 +233,7 @@ var DefaultNcpAgentConversationStateManager = class {
285
233
  }
286
234
  handleMessageToolCallStart(payload) {
287
235
  const targetMessage = this.resolveToolCallTargetMessage(
288
- payload.sessionKey,
236
+ payload.sessionId,
289
237
  payload.toolCallId,
290
238
  payload.messageId
291
239
  );
@@ -306,16 +254,16 @@ var DefaultNcpAgentConversationStateManager = class {
306
254
  }
307
255
  handleMessageToolCallArgs(payload) {
308
256
  this.toolCallArgsRawByCallId.set(payload.toolCallId, payload.args);
309
- this.applyToolCallArgs(payload.sessionKey, payload.toolCallId, payload.args);
257
+ this.applyToolCallArgs(payload.sessionId, payload.toolCallId, payload.args);
310
258
  }
311
259
  handleMessageToolCallArgsDelta(payload) {
312
260
  const currentArgs = this.toolCallArgsRawByCallId.get(payload.toolCallId) ?? "";
313
261
  const nextArgs = `${currentArgs}${payload.delta}`;
314
262
  this.toolCallArgsRawByCallId.set(payload.toolCallId, nextArgs);
315
- this.applyToolCallArgs(payload.sessionKey, payload.toolCallId, nextArgs, payload.messageId);
263
+ this.applyToolCallArgs(payload.sessionId, payload.toolCallId, nextArgs, payload.messageId);
316
264
  }
317
265
  handleMessageToolCallEnd(payload) {
318
- const targetMessage = this.resolveToolCallTargetMessage(payload.sessionKey, payload.toolCallId);
266
+ const targetMessage = this.resolveToolCallTargetMessage(payload.sessionId, payload.toolCallId);
319
267
  const args = this.toolCallArgsRawByCallId.get(payload.toolCallId) ?? "";
320
268
  const nextParts = this.upsertToolInvocationPart(targetMessage.parts, {
321
269
  type: "tool-invocation",
@@ -343,7 +291,7 @@ var DefaultNcpAgentConversationStateManager = class {
343
291
  return this.upsertToolInvocationPart(targetMessage.parts, mergedPart);
344
292
  });
345
293
  if (!updated) {
346
- const fallbackMessage = this.resolveToolCallTargetMessage(payload.sessionKey, payload.toolCallId);
294
+ const fallbackMessage = this.resolveToolCallTargetMessage(payload.sessionId, payload.toolCallId);
347
295
  const nextParts = this.upsertToolInvocationPart(fallbackMessage.parts, {
348
296
  type: "tool-invocation",
349
297
  toolCallId: payload.toolCallId,
@@ -358,21 +306,59 @@ var DefaultNcpAgentConversationStateManager = class {
358
306
  });
359
307
  }
360
308
  }
361
- handleRunStarted(_payload) {
309
+ handleRunStarted(payload) {
362
310
  this.setError(null);
311
+ this.activeRun = {
312
+ runId: payload.runId ?? null,
313
+ sessionId: payload.sessionId
314
+ };
315
+ this.stateVersion += 1;
363
316
  }
364
317
  handleRunFinished(_payload) {
318
+ if (this.streamingMessage) {
319
+ const finalizedMessage = {
320
+ ...this.streamingMessage,
321
+ status: "final"
322
+ };
323
+ this.upsertMessage(finalizedMessage);
324
+ this.replaceStreamingMessage(null);
325
+ this.clearToolCallTrackingByMessageId(finalizedMessage.id);
326
+ }
327
+ this.setError(null);
328
+ this.clearActiveRun();
365
329
  }
366
330
  handleRunError(payload) {
331
+ if (this.streamingMessage) {
332
+ const failedMessage = {
333
+ ...this.streamingMessage,
334
+ status: "error"
335
+ };
336
+ this.upsertMessage(failedMessage);
337
+ this.replaceStreamingMessage(null);
338
+ this.clearToolCallTrackingByMessageId(failedMessage.id);
339
+ }
367
340
  this.setError(buildRuntimeError(payload));
341
+ this.clearActiveRun();
368
342
  }
369
- handleRunMetadata(_payload) {
343
+ handleRunMetadata(payload) {
344
+ const m = payload.metadata;
345
+ if (m?.kind === "ready") {
346
+ const ready = m;
347
+ this.activeRun = {
348
+ runId: ready.runId ?? this.activeRun?.runId ?? null,
349
+ sessionId: ready.sessionId ?? this.activeRun?.sessionId,
350
+ abortDisabledReason: ready.supportsAbort === false ? ready.abortDisabledReason ?? "Unsupported" : null
351
+ };
352
+ this.stateVersion += 1;
353
+ } else if (m?.kind === "final") {
354
+ this.clearActiveRun();
355
+ }
370
356
  }
371
357
  handleEndpointError(payload) {
372
358
  this.setError(payload);
373
359
  }
374
- applyToolCallArgs(sessionKey, toolCallId, args, messageId) {
375
- const targetMessage = this.resolveToolCallTargetMessage(sessionKey, toolCallId, messageId);
360
+ applyToolCallArgs(sessionId, toolCallId, args, messageId) {
361
+ const targetMessage = this.resolveToolCallTargetMessage(sessionId, toolCallId, messageId);
376
362
  const toolName = this.findToolNameByCallId(targetMessage.parts, toolCallId) ?? "unknown";
377
363
  const nextParts = this.upsertToolInvocationPart(targetMessage.parts, {
378
364
  type: "tool-invocation",
@@ -387,7 +373,7 @@ var DefaultNcpAgentConversationStateManager = class {
387
373
  status: "streaming"
388
374
  });
389
375
  }
390
- ensureStreamingMessage(sessionKey, messageId, status) {
376
+ ensureStreamingMessage(sessionId, messageId, status) {
391
377
  if (this.streamingMessage?.id === messageId) {
392
378
  if (this.streamingMessage.status === status) {
393
379
  return this.streamingMessage;
@@ -399,9 +385,24 @@ var DefaultNcpAgentConversationStateManager = class {
399
385
  this.replaceStreamingMessage(nextStreamingMessage2);
400
386
  return nextStreamingMessage2;
401
387
  }
388
+ const messageIndex = this.messages.findIndex((message) => message.id === messageId);
389
+ if (messageIndex >= 0) {
390
+ const existingMessage = cloneMessage(this.messages[messageIndex]);
391
+ const nextMessages = [...this.messages];
392
+ nextMessages.splice(messageIndex, 1);
393
+ this.messages = nextMessages;
394
+ this.stateVersion += 1;
395
+ const nextStreamingMessage2 = {
396
+ ...existingMessage,
397
+ sessionId,
398
+ status
399
+ };
400
+ this.replaceStreamingMessage(nextStreamingMessage2);
401
+ return nextStreamingMessage2;
402
+ }
402
403
  const nextStreamingMessage = {
403
404
  id: messageId,
404
- sessionKey,
405
+ sessionId,
405
406
  role: DEFAULT_ASSISTANT_ROLE,
406
407
  status,
407
408
  parts: [],
@@ -410,10 +411,10 @@ var DefaultNcpAgentConversationStateManager = class {
410
411
  this.replaceStreamingMessage(nextStreamingMessage);
411
412
  return nextStreamingMessage;
412
413
  }
413
- resolveToolCallTargetMessage(sessionKey, toolCallId, messageId) {
414
+ resolveToolCallTargetMessage(sessionId, toolCallId, messageId) {
414
415
  const preferredMessageId = messageId?.trim() || this.toolCallMessageIdByCallId.get(toolCallId) || this.streamingMessage?.id || `tool-${toolCallId}`;
415
416
  this.toolCallMessageIdByCallId.set(toolCallId, preferredMessageId);
416
- return this.ensureStreamingMessage(sessionKey, preferredMessageId, "streaming");
417
+ return this.ensureStreamingMessage(sessionId, preferredMessageId, "streaming");
417
418
  }
418
419
  updateMessageContainingToolCall(toolCallId, updater) {
419
420
  if (this.streamingMessage) {
@@ -469,14 +470,6 @@ var DefaultNcpAgentConversationStateManager = class {
469
470
  nextParts.push(toolPart);
470
471
  return nextParts;
471
472
  }
472
- findLastReasoningPartIndex(parts) {
473
- for (let index = parts.length - 1; index >= 0; index -= 1) {
474
- if (parts[index]?.type === "reasoning") {
475
- return index;
476
- }
477
- }
478
- return -1;
479
- }
480
473
  upsertMessage(message) {
481
474
  const normalizedMessage = cloneMessage(message);
482
475
  const messageIndex = this.messages.findIndex((item) => item.id === normalizedMessage.id);
@@ -505,6 +498,13 @@ var DefaultNcpAgentConversationStateManager = class {
505
498
  this.error = nextError ? { ...nextError, details: nextError.details ? { ...nextError.details } : void 0 } : null;
506
499
  this.stateVersion += 1;
507
500
  }
501
+ clearActiveRun() {
502
+ if (!this.activeRun) {
503
+ return;
504
+ }
505
+ this.activeRun = null;
506
+ this.stateVersion += 1;
507
+ }
508
508
  clearToolCallTrackingByMessageId(messageId) {
509
509
  for (const [toolCallId, trackedMessageId] of this.toolCallMessageIdByCallId) {
510
510
  if (trackedMessageId !== messageId) {
@@ -514,7 +514,638 @@ var DefaultNcpAgentConversationStateManager = class {
514
514
  this.toolCallArgsRawByCallId.delete(toolCallId);
515
515
  }
516
516
  }
517
+ notifyListeners() {
518
+ const snapshot = this.getSnapshot();
519
+ for (const listener of this.listeners) {
520
+ listener(snapshot);
521
+ }
522
+ }
523
+ };
524
+
525
+ // src/agent/agent-client-from-server.ts
526
+ import {
527
+ NcpEventType as NcpEventType2
528
+ } from "@nextclaw/ncp";
529
+ function createAgentClientFromServer(server) {
530
+ return {
531
+ get manifest() {
532
+ return server.manifest;
533
+ },
534
+ async start() {
535
+ await server.start();
536
+ },
537
+ async stop() {
538
+ await server.stop();
539
+ },
540
+ async emit(event) {
541
+ switch (event.type) {
542
+ case NcpEventType2.MessageRequest:
543
+ await consume(server.send(event.payload));
544
+ return;
545
+ case NcpEventType2.MessageStreamRequest:
546
+ await consume(server.stream(event.payload));
547
+ return;
548
+ case NcpEventType2.MessageAbort:
549
+ await server.abort(event.payload);
550
+ return;
551
+ default:
552
+ await server.emit(event);
553
+ }
554
+ },
555
+ subscribe(listener) {
556
+ return server.subscribe(listener);
557
+ },
558
+ async send(envelope) {
559
+ await consume(server.send(envelope));
560
+ },
561
+ async stream(payload) {
562
+ await consume(server.stream(payload));
563
+ },
564
+ async abort(payload) {
565
+ await server.abort(payload);
566
+ }
567
+ };
568
+ }
569
+ async function consume(events) {
570
+ for await (const event of events) {
571
+ void event;
572
+ }
573
+ }
574
+
575
+ // src/agent/agent-backend/agent-backend.ts
576
+ import {
577
+ NcpEventType as NcpEventType4
578
+ } from "@nextclaw/ncp";
579
+
580
+ // src/errors/ncp-error-exception.ts
581
+ var NcpErrorException = class extends Error {
582
+ code;
583
+ details;
584
+ constructor(code, message, details) {
585
+ super(message);
586
+ this.name = "NcpErrorException";
587
+ this.code = code;
588
+ this.details = details;
589
+ }
590
+ };
591
+
592
+ // src/agent/agent-backend/agent-live-session-registry.ts
593
+ var AgentLiveSessionRegistry = class {
594
+ constructor(sessionStore, createRuntime) {
595
+ this.sessionStore = sessionStore;
596
+ this.createRuntime = createRuntime;
597
+ }
598
+ sessions = /* @__PURE__ */ new Map();
599
+ async ensureSession(sessionId) {
600
+ const existing = this.sessions.get(sessionId);
601
+ if (existing) {
602
+ return existing;
603
+ }
604
+ const storedSession = await this.sessionStore.getSession(sessionId);
605
+ const stateManager = new DefaultNcpAgentConversationStateManager();
606
+ stateManager.hydrate({
607
+ sessionId,
608
+ messages: cloneMessages(storedSession?.messages ?? [])
609
+ });
610
+ const session = {
611
+ sessionId,
612
+ stateManager,
613
+ runtime: this.createRuntime({ sessionId, stateManager }),
614
+ activeExecution: null
615
+ };
616
+ this.sessions.set(sessionId, session);
617
+ return session;
618
+ }
619
+ getSession(sessionId) {
620
+ return this.sessions.get(sessionId) ?? null;
621
+ }
622
+ deleteSession(sessionId) {
623
+ const session = this.sessions.get(sessionId) ?? null;
624
+ if (session) {
625
+ this.sessions.delete(sessionId);
626
+ }
627
+ return session;
628
+ }
629
+ clear() {
630
+ this.sessions.clear();
631
+ }
632
+ listSessions() {
633
+ return [...this.sessions.values()];
634
+ }
635
+ };
636
+ function cloneMessages(messages) {
637
+ return messages.map((message) => structuredClone(message));
638
+ }
639
+
640
+ // src/agent/agent-backend/agent-run-executor.ts
641
+ import { NcpEventType as NcpEventType3 } from "@nextclaw/ncp";
642
+ var AgentRunExecutor = class {
643
+ constructor(persistSession) {
644
+ this.persistSession = persistSession;
645
+ }
646
+ async *executeRun(session, envelope, controller) {
647
+ const messageSent = {
648
+ type: NcpEventType3.MessageSent,
649
+ payload: {
650
+ sessionId: envelope.sessionId,
651
+ message: structuredClone(envelope.message),
652
+ metadata: envelope.metadata
653
+ }
654
+ };
655
+ let messageSentPublished = false;
656
+ try {
657
+ for await (const event of session.runtime.run(
658
+ {
659
+ sessionId: envelope.sessionId,
660
+ messages: [envelope.message],
661
+ correlationId: envelope.correlationId
662
+ },
663
+ { signal: controller.signal }
664
+ )) {
665
+ if (!messageSentPublished) {
666
+ messageSentPublished = true;
667
+ await this.persistSession(envelope.sessionId);
668
+ yield structuredClone(messageSent);
669
+ }
670
+ await this.persistSession(envelope.sessionId);
671
+ yield structuredClone(event);
672
+ }
673
+ } catch (error) {
674
+ if (!controller.signal.aborted) {
675
+ const runErrorEvent = await this.publishFailure(error, envelope, session);
676
+ yield structuredClone(runErrorEvent);
677
+ }
678
+ } finally {
679
+ await this.persistSession(envelope.sessionId);
680
+ }
681
+ }
682
+ async publishFailure(error, envelope, session) {
683
+ const message = error instanceof Error ? error.message : String(error);
684
+ const runErrorEvent = {
685
+ type: NcpEventType3.RunError,
686
+ payload: {
687
+ sessionId: envelope.sessionId,
688
+ error: message
689
+ }
690
+ };
691
+ await session.stateManager.dispatch(runErrorEvent);
692
+ await this.persistSession(envelope.sessionId);
693
+ return runErrorEvent;
694
+ }
695
+ };
696
+
697
+ // src/agent/agent-backend/async-queue.ts
698
+ function createAsyncQueue() {
699
+ const items = [];
700
+ let closed = false;
701
+ let pendingResolve = null;
702
+ const iterable = {
703
+ [Symbol.asyncIterator]() {
704
+ return {
705
+ next: () => {
706
+ if (items.length > 0) {
707
+ return Promise.resolve({
708
+ value: items.shift(),
709
+ done: false
710
+ });
711
+ }
712
+ if (closed) {
713
+ return Promise.resolve({
714
+ value: void 0,
715
+ done: true
716
+ });
717
+ }
718
+ return new Promise((resolve) => {
719
+ pendingResolve = resolve;
720
+ });
721
+ }
722
+ };
723
+ }
724
+ };
725
+ return {
726
+ push(item) {
727
+ if (closed) {
728
+ return;
729
+ }
730
+ if (pendingResolve) {
731
+ const resolve = pendingResolve;
732
+ pendingResolve = null;
733
+ resolve({ value: item, done: false });
734
+ return;
735
+ }
736
+ items.push(item);
737
+ },
738
+ close() {
739
+ if (closed) {
740
+ return;
741
+ }
742
+ closed = true;
743
+ if (pendingResolve) {
744
+ const resolve = pendingResolve;
745
+ pendingResolve = null;
746
+ resolve({
747
+ value: void 0,
748
+ done: true
749
+ });
750
+ }
751
+ },
752
+ iterable
753
+ };
754
+ }
755
+
756
+ // src/agent/agent-backend/event-publisher.ts
757
+ var EventPublisher = class {
758
+ listeners = /* @__PURE__ */ new Set();
759
+ closeListeners = /* @__PURE__ */ new Set();
760
+ closed = false;
761
+ subscribe(listener) {
762
+ if (this.closed) {
763
+ return () => void 0;
764
+ }
765
+ this.listeners.add(listener);
766
+ return () => {
767
+ this.listeners.delete(listener);
768
+ };
769
+ }
770
+ onClose(listener) {
771
+ if (this.closed) {
772
+ listener();
773
+ return () => void 0;
774
+ }
775
+ this.closeListeners.add(listener);
776
+ return () => {
777
+ this.closeListeners.delete(listener);
778
+ };
779
+ }
780
+ publish(event) {
781
+ if (this.closed) {
782
+ return;
783
+ }
784
+ for (const listener of this.listeners) {
785
+ listener(structuredClone(event));
786
+ }
787
+ }
788
+ close() {
789
+ if (this.closed) {
790
+ return;
791
+ }
792
+ this.closed = true;
793
+ this.listeners.clear();
794
+ for (const listener of this.closeListeners) {
795
+ listener();
796
+ }
797
+ this.closeListeners.clear();
798
+ }
799
+ };
800
+
801
+ // src/agent/agent-backend/agent-backend.ts
802
+ var DEFAULT_SUPPORTED_PART_TYPES = [
803
+ "text",
804
+ "file",
805
+ "source",
806
+ "step-start",
807
+ "reasoning",
808
+ "tool-invocation",
809
+ "card",
810
+ "rich-text",
811
+ "action",
812
+ "extension"
813
+ ];
814
+ var DefaultNcpAgentBackend = class {
815
+ manifest;
816
+ sessionStore;
817
+ sessionRegistry;
818
+ executor;
819
+ publisher;
820
+ started = false;
821
+ constructor(config) {
822
+ this.sessionStore = config.sessionStore;
823
+ this.sessionRegistry = new AgentLiveSessionRegistry(
824
+ this.sessionStore,
825
+ config.createRuntime
826
+ );
827
+ this.executor = new AgentRunExecutor(async (sessionId) => this.persistSession(sessionId));
828
+ this.publisher = new EventPublisher();
829
+ this.manifest = {
830
+ endpointKind: "agent",
831
+ endpointId: config.endpointId?.trim() || "ncp-agent-backend",
832
+ version: config.version?.trim() || "0.1.0",
833
+ supportsStreaming: true,
834
+ supportsAbort: true,
835
+ supportsProactiveMessages: false,
836
+ supportsLiveSessionStream: true,
837
+ supportedPartTypes: config.supportedPartTypes ?? DEFAULT_SUPPORTED_PART_TYPES,
838
+ expectedLatency: config.expectedLatency ?? "seconds",
839
+ metadata: config.metadata
840
+ };
841
+ }
842
+ async start() {
843
+ if (this.started) {
844
+ return;
845
+ }
846
+ this.started = true;
847
+ this.publisher.publish({ type: NcpEventType4.EndpointReady });
848
+ }
849
+ async stop() {
850
+ if (!this.started) {
851
+ return;
852
+ }
853
+ this.started = false;
854
+ for (const session of this.sessionRegistry.listSessions()) {
855
+ const execution = session.activeExecution;
856
+ if (!execution) {
857
+ continue;
858
+ }
859
+ execution.abortHandled = true;
860
+ execution.controller.abort();
861
+ this.finishSessionExecution(session, execution);
862
+ }
863
+ this.sessionRegistry.clear();
864
+ }
865
+ async emit(event) {
866
+ await this.ensureStarted();
867
+ switch (event.type) {
868
+ case NcpEventType4.MessageRequest:
869
+ await this.handleRequest(event.payload);
870
+ return;
871
+ case NcpEventType4.MessageStreamRequest:
872
+ await this.streamToSubscribers(event.payload);
873
+ return;
874
+ case NcpEventType4.MessageAbort:
875
+ await this.handleAbort(event.payload);
876
+ return;
877
+ default:
878
+ this.publisher.publish(event);
879
+ }
880
+ }
881
+ subscribe(listener) {
882
+ return this.publisher.subscribe(listener);
883
+ }
884
+ async *send(envelope, options) {
885
+ await this.ensureStarted();
886
+ const session = await this.sessionRegistry.ensureSession(envelope.sessionId);
887
+ const execution = this.startSessionExecution(session, envelope, options?.signal);
888
+ try {
889
+ for await (const event of this.executor.executeRun(session, envelope, execution.controller)) {
890
+ this.publishLiveEvent(execution, event);
891
+ yield event;
892
+ }
893
+ if (execution.controller.signal.aborted && !execution.abortHandled) {
894
+ const abortEvent = await this.createAbortEvent(session.sessionId);
895
+ execution.abortHandled = true;
896
+ this.publishLiveEvent(execution, abortEvent);
897
+ yield abortEvent;
898
+ }
899
+ } finally {
900
+ this.finishSessionExecution(session, execution);
901
+ }
902
+ }
903
+ async abort(payload) {
904
+ await this.handleAbort(payload);
905
+ }
906
+ async *stream(payloadOrParams, opts) {
907
+ const payload = "payload" in payloadOrParams && "signal" in payloadOrParams ? payloadOrParams.payload : payloadOrParams;
908
+ const signal = "payload" in payloadOrParams && "signal" in payloadOrParams ? payloadOrParams.signal : opts?.signal ?? new AbortController().signal;
909
+ const session = this.sessionRegistry.getSession(payload.sessionId);
910
+ const execution = session?.activeExecution;
911
+ if (!session || !execution || execution.closed) {
912
+ return;
913
+ }
914
+ const queue = createAsyncQueue();
915
+ const unsubscribe = execution.publisher.subscribe((event) => {
916
+ queue.push(event);
917
+ });
918
+ const unsubscribeClose = execution.publisher.onClose(() => {
919
+ queue.close();
920
+ });
921
+ const stop = () => {
922
+ unsubscribe();
923
+ unsubscribeClose();
924
+ queue.close();
925
+ signal.removeEventListener("abort", stop);
926
+ };
927
+ signal.addEventListener("abort", stop, { once: true });
928
+ try {
929
+ for await (const event of queue.iterable) {
930
+ if (signal.aborted) {
931
+ break;
932
+ }
933
+ yield event;
934
+ if (isTerminalEvent(event)) {
935
+ break;
936
+ }
937
+ }
938
+ } finally {
939
+ stop();
940
+ }
941
+ }
942
+ async listSessions() {
943
+ const storedSessions = await this.sessionStore.listSessions();
944
+ const summaries = storedSessions.map(
945
+ (session) => toSessionSummary(session, this.sessionRegistry.getSession(session.sessionId))
946
+ );
947
+ for (const liveSession of this.sessionRegistry.listSessions()) {
948
+ if (summaries.some((session) => session.sessionId === liveSession.sessionId)) {
949
+ continue;
950
+ }
951
+ summaries.push(toLiveSessionSummary(liveSession));
952
+ }
953
+ return summaries.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
954
+ }
955
+ async listSessionMessages(sessionId) {
956
+ const liveSession = this.sessionRegistry.getSession(sessionId);
957
+ if (liveSession) {
958
+ return readMessages(liveSession.stateManager.getSnapshot());
959
+ }
960
+ const session = await this.sessionStore.getSession(sessionId);
961
+ return session ? session.messages.map((message) => structuredClone(message)) : [];
962
+ }
963
+ async getSession(sessionId) {
964
+ const liveSession = this.sessionRegistry.getSession(sessionId);
965
+ const storedSession = await this.sessionStore.getSession(sessionId);
966
+ if (storedSession) {
967
+ return toSessionSummary(storedSession, liveSession);
968
+ }
969
+ if (liveSession) {
970
+ return toLiveSessionSummary(liveSession);
971
+ }
972
+ return null;
973
+ }
974
+ async deleteSession(sessionId) {
975
+ const liveSession = this.sessionRegistry.deleteSession(sessionId);
976
+ const execution = liveSession?.activeExecution;
977
+ if (execution) {
978
+ execution.abortHandled = true;
979
+ execution.controller.abort();
980
+ this.closeExecution(execution);
981
+ }
982
+ await this.sessionStore.deleteSession(sessionId);
983
+ }
984
+ async ensureStarted() {
985
+ if (!this.started) {
986
+ await this.start();
987
+ }
988
+ }
989
+ startSessionExecution(session, envelope, signal) {
990
+ if (session.activeExecution && !session.activeExecution.closed) {
991
+ throw new NcpErrorException(
992
+ "runtime-error",
993
+ `Session ${session.sessionId} already has an active execution.`,
994
+ { sessionId: session.sessionId }
995
+ );
996
+ }
997
+ const controller = new AbortController();
998
+ if (signal) {
999
+ signal.addEventListener("abort", () => controller.abort(), {
1000
+ once: true
1001
+ });
1002
+ }
1003
+ const execution = {
1004
+ controller,
1005
+ publisher: new EventPublisher(),
1006
+ requestEnvelope: structuredClone(envelope),
1007
+ abortHandled: false,
1008
+ closed: false
1009
+ };
1010
+ session.activeExecution = execution;
1011
+ return execution;
1012
+ }
1013
+ finishSessionExecution(session, execution) {
1014
+ if (session.activeExecution === execution) {
1015
+ session.activeExecution = null;
1016
+ }
1017
+ this.closeExecution(execution);
1018
+ }
1019
+ closeExecution(execution) {
1020
+ if (execution.closed) {
1021
+ return;
1022
+ }
1023
+ execution.closed = true;
1024
+ execution.publisher.close();
1025
+ }
1026
+ publishLiveEvent(execution, event) {
1027
+ this.publisher.publish(event);
1028
+ if (!execution.closed) {
1029
+ execution.publisher.publish(event);
1030
+ }
1031
+ }
1032
+ async handleRequest(envelope) {
1033
+ for await (const event of this.send(envelope)) {
1034
+ void event;
1035
+ }
1036
+ }
1037
+ async streamToSubscribers(payload) {
1038
+ const signal = new AbortController().signal;
1039
+ for await (const event of this.stream({ payload, signal })) {
1040
+ void event;
1041
+ }
1042
+ }
1043
+ async handleAbort(payload) {
1044
+ const session = this.sessionRegistry.getSession(payload.sessionId);
1045
+ const execution = session?.activeExecution;
1046
+ if (!session || !execution || execution.closed) {
1047
+ return;
1048
+ }
1049
+ execution.abortHandled = true;
1050
+ execution.controller.abort();
1051
+ const abortEvent = await this.createAbortEvent(payload.sessionId, payload.messageId);
1052
+ this.publishLiveEvent(execution, abortEvent);
1053
+ this.finishSessionExecution(session, execution);
1054
+ }
1055
+ async createAbortEvent(sessionId, messageId) {
1056
+ const abortEvent = {
1057
+ type: NcpEventType4.MessageAbort,
1058
+ payload: {
1059
+ sessionId,
1060
+ ...messageId ? { messageId } : {}
1061
+ }
1062
+ };
1063
+ const liveSession = this.sessionRegistry.getSession(sessionId);
1064
+ if (liveSession) {
1065
+ await liveSession.stateManager.dispatch(abortEvent);
1066
+ }
1067
+ await this.persistSession(sessionId);
1068
+ return abortEvent;
1069
+ }
1070
+ async persistSession(sessionId) {
1071
+ const session = this.sessionRegistry.getSession(sessionId);
1072
+ if (!session) {
1073
+ return;
1074
+ }
1075
+ const snapshot = session.stateManager.getSnapshot();
1076
+ await this.sessionStore.saveSession({
1077
+ sessionId,
1078
+ messages: readMessages(snapshot),
1079
+ updatedAt: now()
1080
+ });
1081
+ }
1082
+ };
1083
+ function readMessages(snapshot) {
1084
+ const messages = snapshot.messages.map((message) => structuredClone(message));
1085
+ if (snapshot.streamingMessage) {
1086
+ messages.push(structuredClone(snapshot.streamingMessage));
1087
+ }
1088
+ return messages;
1089
+ }
1090
+ function toSessionSummary(session, liveSession) {
1091
+ return {
1092
+ sessionId: session.sessionId,
1093
+ messageCount: session.messages.length,
1094
+ updatedAt: session.updatedAt,
1095
+ status: liveSession?.activeExecution ? "running" : "idle"
1096
+ };
1097
+ }
1098
+ function toLiveSessionSummary(session) {
1099
+ const snapshot = session.stateManager.getSnapshot();
1100
+ return {
1101
+ sessionId: session.sessionId,
1102
+ messageCount: readMessages(snapshot).length,
1103
+ updatedAt: now(),
1104
+ status: session.activeExecution ? "running" : "idle"
1105
+ };
1106
+ }
1107
+ function now() {
1108
+ return (/* @__PURE__ */ new Date()).toISOString();
1109
+ }
1110
+ function isTerminalEvent(event) {
1111
+ switch (event.type) {
1112
+ case NcpEventType4.MessageAbort:
1113
+ case NcpEventType4.RunFinished:
1114
+ case NcpEventType4.RunError:
1115
+ return true;
1116
+ default:
1117
+ return false;
1118
+ }
1119
+ }
1120
+
1121
+ // src/agent/agent-backend/in-memory-agent-session-store.ts
1122
+ var InMemoryAgentSessionStore = class {
1123
+ sessions = /* @__PURE__ */ new Map();
1124
+ async getSession(sessionId) {
1125
+ const session = this.sessions.get(sessionId);
1126
+ return session ? structuredClone(session) : null;
1127
+ }
1128
+ async listSessions() {
1129
+ return [...this.sessions.values()].map((session) => structuredClone(session));
1130
+ }
1131
+ async saveSession(session) {
1132
+ this.sessions.set(session.sessionId, structuredClone(session));
1133
+ }
1134
+ async deleteSession(sessionId) {
1135
+ const session = this.sessions.get(sessionId);
1136
+ if (!session) {
1137
+ return null;
1138
+ }
1139
+ this.sessions.delete(sessionId);
1140
+ return structuredClone(session);
1141
+ }
517
1142
  };
518
1143
  export {
519
- DefaultNcpAgentConversationStateManager
1144
+ AgentRunExecutor,
1145
+ DefaultNcpAgentBackend,
1146
+ DefaultNcpAgentConversationStateManager,
1147
+ EventPublisher,
1148
+ InMemoryAgentSessionStore,
1149
+ NcpErrorException,
1150
+ createAgentClientFromServer
520
1151
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ncp-toolkit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "Toolkit implementations built on top of the NextClaw Communication Protocol.",
6
6
  "type": "module",
@@ -15,18 +15,15 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@nextclaw/ncp": "0.1.0"
18
+ "@nextclaw/ncp": "0.2.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^20.17.6",
22
- "@typescript-eslint/eslint-plugin": "^7.18.0",
23
- "@typescript-eslint/parser": "^7.18.0",
24
- "eslint": "^8.57.1",
25
- "eslint-config-prettier": "^9.1.0",
26
22
  "prettier": "^3.3.3",
27
23
  "tsup": "^8.3.5",
28
24
  "typescript": "^5.6.3",
29
- "vitest": "^2.1.2"
25
+ "vitest": "^2.1.2",
26
+ "@nextclaw/ncp-agent-runtime": "0.1.1"
30
27
  },
31
28
  "scripts": {
32
29
  "build": "tsup src/index.ts --format esm --dts --out-dir dist",