@nextclaw/ncp-toolkit 0.1.1 → 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 +5 -1
- package/dist/index.d.ts +123 -8
- package/dist/index.js +746 -141
- package/package.json +4 -7
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,4 +1,4 @@
|
|
|
1
|
-
import { NcpAgentConversationStateManager, NcpAgentConversationSnapshot,
|
|
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;
|
|
@@ -8,16 +8,15 @@ declare class DefaultNcpAgentConversationStateManager implements NcpAgentConvers
|
|
|
8
8
|
private readonly listeners;
|
|
9
9
|
private readonly toolCallMessageIdByCallId;
|
|
10
10
|
private readonly toolCallArgsRawByCallId;
|
|
11
|
+
private snapshotCache;
|
|
12
|
+
private snapshotVersion;
|
|
11
13
|
private stateVersion;
|
|
12
14
|
getSnapshot(): NcpAgentConversationSnapshot;
|
|
13
15
|
subscribe(listener: (snapshot: NcpAgentConversationSnapshot) => void): () => boolean;
|
|
16
|
+
reset(): void;
|
|
17
|
+
hydrate(payload: NcpAgentConversationHydrationParams): void;
|
|
14
18
|
dispatch(event: NcpEndpointEvent): Promise<void>;
|
|
15
|
-
handleMessageRequest(payload: NcpRequestEnvelope): void;
|
|
16
19
|
handleMessageSent(payload: NcpMessageSentPayload): void;
|
|
17
|
-
handleMessageAccepted(_payload: NcpMessageAcceptedPayload): void;
|
|
18
|
-
handleMessageIncoming(payload: NcpResponseEnvelope): void;
|
|
19
|
-
handleMessageCompleted(payload: NcpCompletedEnvelope): void;
|
|
20
|
-
handleMessageFailed(payload: NcpFailedEnvelope): void;
|
|
21
20
|
handleMessageAbort(payload: NcpMessageAbortPayload): void;
|
|
22
21
|
handleMessageTextStart(payload: NcpTextStartPayload): void;
|
|
23
22
|
handleMessageTextDelta(payload: NcpTextDeltaPayload): void;
|
|
@@ -42,11 +41,127 @@ declare class DefaultNcpAgentConversationStateManager implements NcpAgentConvers
|
|
|
42
41
|
private findToolInvocationPart;
|
|
43
42
|
private findToolNameByCallId;
|
|
44
43
|
private upsertToolInvocationPart;
|
|
45
|
-
private findLastReasoningPartIndex;
|
|
46
44
|
private upsertMessage;
|
|
47
45
|
private replaceStreamingMessage;
|
|
48
46
|
private setError;
|
|
47
|
+
private clearActiveRun;
|
|
49
48
|
private clearToolCallTrackingByMessageId;
|
|
49
|
+
private notifyListeners;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
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 {
|
|
@@ -28,193 +31,137 @@ var DefaultNcpAgentConversationStateManager = class {
|
|
|
28
31
|
listeners = /* @__PURE__ */ new Set();
|
|
29
32
|
toolCallMessageIdByCallId = /* @__PURE__ */ new Map();
|
|
30
33
|
toolCallArgsRawByCallId = /* @__PURE__ */ new Map();
|
|
34
|
+
snapshotCache = null;
|
|
35
|
+
snapshotVersion = -1;
|
|
31
36
|
stateVersion = 0;
|
|
32
37
|
getSnapshot() {
|
|
33
|
-
|
|
38
|
+
if (this.snapshotCache && this.snapshotVersion === this.stateVersion) {
|
|
39
|
+
return this.snapshotCache;
|
|
40
|
+
}
|
|
41
|
+
const snapshot = {
|
|
34
42
|
messages: this.messages.map((message) => cloneMessage(message)),
|
|
35
43
|
streamingMessage: this.streamingMessage ? cloneMessage(this.streamingMessage) : null,
|
|
36
44
|
error: this.error ? { ...this.error, details: this.error.details ? { ...this.error.details } : void 0 } : null,
|
|
37
45
|
activeRun: this.activeRun ? { ...this.activeRun } : null
|
|
38
46
|
};
|
|
47
|
+
this.snapshotCache = snapshot;
|
|
48
|
+
this.snapshotVersion = this.stateVersion;
|
|
49
|
+
return snapshot;
|
|
39
50
|
}
|
|
40
51
|
subscribe(listener) {
|
|
41
52
|
this.listeners.add(listener);
|
|
42
53
|
return () => this.listeners.delete(listener);
|
|
43
54
|
}
|
|
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
|
+
}
|
|
44
82
|
async dispatch(event) {
|
|
45
83
|
const versionBeforeDispatch = this.stateVersion;
|
|
46
84
|
switch (event.type) {
|
|
47
|
-
case
|
|
48
|
-
this.handleMessageRequest(event.payload);
|
|
49
|
-
break;
|
|
50
|
-
case "message.resume-request":
|
|
51
|
-
break;
|
|
52
|
-
case "message.sent":
|
|
85
|
+
case NcpEventType.MessageSent:
|
|
53
86
|
this.handleMessageSent(event.payload);
|
|
54
87
|
break;
|
|
55
|
-
case
|
|
56
|
-
this.handleMessageAccepted(event.payload);
|
|
57
|
-
break;
|
|
58
|
-
case "message.incoming":
|
|
59
|
-
this.handleMessageIncoming(event.payload);
|
|
60
|
-
break;
|
|
61
|
-
case "message.completed":
|
|
62
|
-
this.handleMessageCompleted(event.payload);
|
|
63
|
-
break;
|
|
64
|
-
case "message.failed":
|
|
65
|
-
this.handleMessageFailed(event.payload);
|
|
66
|
-
break;
|
|
67
|
-
case "message.abort":
|
|
88
|
+
case NcpEventType.MessageAbort:
|
|
68
89
|
this.handleMessageAbort(event.payload);
|
|
69
90
|
break;
|
|
70
|
-
case
|
|
91
|
+
case NcpEventType.MessageTextStart:
|
|
71
92
|
this.handleMessageTextStart(event.payload);
|
|
72
93
|
break;
|
|
73
|
-
case
|
|
94
|
+
case NcpEventType.MessageTextDelta:
|
|
74
95
|
this.handleMessageTextDelta(event.payload);
|
|
75
96
|
break;
|
|
76
|
-
case
|
|
97
|
+
case NcpEventType.MessageTextEnd:
|
|
77
98
|
this.handleMessageTextEnd(event.payload);
|
|
78
99
|
break;
|
|
79
|
-
case
|
|
100
|
+
case NcpEventType.MessageReasoningStart:
|
|
80
101
|
this.handleMessageReasoningStart(event.payload);
|
|
81
102
|
break;
|
|
82
|
-
case
|
|
103
|
+
case NcpEventType.MessageReasoningDelta:
|
|
83
104
|
this.handleMessageReasoningDelta(event.payload);
|
|
84
105
|
break;
|
|
85
|
-
case
|
|
106
|
+
case NcpEventType.MessageReasoningEnd:
|
|
86
107
|
this.handleMessageReasoningEnd(event.payload);
|
|
87
108
|
break;
|
|
88
|
-
case
|
|
109
|
+
case NcpEventType.MessageToolCallStart:
|
|
89
110
|
this.handleMessageToolCallStart(event.payload);
|
|
90
111
|
break;
|
|
91
|
-
case
|
|
112
|
+
case NcpEventType.MessageToolCallArgs:
|
|
92
113
|
this.handleMessageToolCallArgs(event.payload);
|
|
93
114
|
break;
|
|
94
|
-
case
|
|
115
|
+
case NcpEventType.MessageToolCallArgsDelta:
|
|
95
116
|
this.handleMessageToolCallArgsDelta(event.payload);
|
|
96
117
|
break;
|
|
97
|
-
case
|
|
118
|
+
case NcpEventType.MessageToolCallEnd:
|
|
98
119
|
this.handleMessageToolCallEnd(event.payload);
|
|
99
120
|
break;
|
|
100
|
-
case
|
|
121
|
+
case NcpEventType.MessageToolCallResult:
|
|
101
122
|
this.handleMessageToolCallResult(event.payload);
|
|
102
123
|
break;
|
|
103
|
-
case
|
|
124
|
+
case NcpEventType.RunStarted:
|
|
104
125
|
this.handleRunStarted(event.payload);
|
|
105
126
|
break;
|
|
106
|
-
case
|
|
127
|
+
case NcpEventType.RunFinished:
|
|
107
128
|
this.handleRunFinished(event.payload);
|
|
108
129
|
break;
|
|
109
|
-
case
|
|
130
|
+
case NcpEventType.RunError:
|
|
110
131
|
this.handleRunError(event.payload);
|
|
111
132
|
break;
|
|
112
|
-
case
|
|
133
|
+
case NcpEventType.RunMetadata:
|
|
113
134
|
this.handleRunMetadata(event.payload);
|
|
114
135
|
break;
|
|
115
|
-
case
|
|
136
|
+
case NcpEventType.EndpointError:
|
|
116
137
|
this.handleEndpointError(event.payload);
|
|
117
138
|
break;
|
|
118
|
-
case "endpoint.ready":
|
|
119
|
-
case "typing.start":
|
|
120
|
-
case "typing.end":
|
|
121
|
-
case "presence.updated":
|
|
122
|
-
case "message.read":
|
|
123
|
-
case "message.delivered":
|
|
124
|
-
case "message.recalled":
|
|
125
|
-
case "message.reaction":
|
|
126
|
-
break;
|
|
127
139
|
default:
|
|
128
140
|
break;
|
|
129
141
|
}
|
|
130
142
|
if (this.stateVersion !== versionBeforeDispatch) {
|
|
131
|
-
|
|
132
|
-
for (const listener of this.listeners) {
|
|
133
|
-
listener(snapshot);
|
|
134
|
-
}
|
|
143
|
+
this.notifyListeners();
|
|
135
144
|
}
|
|
136
145
|
}
|
|
137
|
-
handleMessageRequest(payload) {
|
|
138
|
-
this.upsertMessage(payload.message);
|
|
139
|
-
this.setError(null);
|
|
140
|
-
}
|
|
141
146
|
handleMessageSent(payload) {
|
|
142
147
|
this.upsertMessage(payload.message);
|
|
143
148
|
this.setError(null);
|
|
144
149
|
}
|
|
145
|
-
handleMessageAccepted(_payload) {
|
|
146
|
-
}
|
|
147
|
-
handleMessageIncoming(payload) {
|
|
148
|
-
const incomingMessage = cloneMessage(payload.message);
|
|
149
|
-
if (incomingMessage.status === "streaming" || incomingMessage.status === "pending") {
|
|
150
|
-
this.replaceStreamingMessage(incomingMessage);
|
|
151
|
-
this.setError(null);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
this.upsertMessage(incomingMessage);
|
|
155
|
-
if (this.streamingMessage?.id === incomingMessage.id) {
|
|
156
|
-
this.replaceStreamingMessage(null);
|
|
157
|
-
this.clearToolCallTrackingByMessageId(incomingMessage.id);
|
|
158
|
-
}
|
|
159
|
-
this.setError(null);
|
|
160
|
-
}
|
|
161
|
-
handleMessageCompleted(payload) {
|
|
162
|
-
const completedMessage = {
|
|
163
|
-
...cloneMessage(payload.message),
|
|
164
|
-
status: "final"
|
|
165
|
-
};
|
|
166
|
-
this.upsertMessage(completedMessage);
|
|
167
|
-
if (this.streamingMessage?.id === completedMessage.id) {
|
|
168
|
-
this.replaceStreamingMessage(null);
|
|
169
|
-
}
|
|
170
|
-
this.clearToolCallTrackingByMessageId(completedMessage.id);
|
|
171
|
-
this.setError(null);
|
|
172
|
-
}
|
|
173
|
-
handleMessageFailed(payload) {
|
|
174
|
-
this.setError(payload.error);
|
|
175
|
-
const targetMessageId = payload.messageId?.trim();
|
|
176
|
-
if (!targetMessageId) {
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (this.streamingMessage?.id === targetMessageId) {
|
|
180
|
-
this.upsertMessage({
|
|
181
|
-
...this.streamingMessage,
|
|
182
|
-
status: "error"
|
|
183
|
-
});
|
|
184
|
-
this.replaceStreamingMessage(null);
|
|
185
|
-
this.clearToolCallTrackingByMessageId(targetMessageId);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
const messageIndex = this.messages.findIndex((message) => message.id === targetMessageId);
|
|
189
|
-
if (messageIndex < 0) {
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const nextMessages = [...this.messages];
|
|
193
|
-
nextMessages[messageIndex] = {
|
|
194
|
-
...nextMessages[messageIndex],
|
|
195
|
-
status: "error"
|
|
196
|
-
};
|
|
197
|
-
this.messages = nextMessages;
|
|
198
|
-
this.stateVersion += 1;
|
|
199
|
-
}
|
|
200
150
|
handleMessageAbort(payload) {
|
|
201
151
|
const targetMessageId = payload.messageId?.trim();
|
|
202
|
-
this.
|
|
203
|
-
|
|
204
|
-
message: "Message aborted.",
|
|
205
|
-
details: {
|
|
206
|
-
messageId: targetMessageId,
|
|
207
|
-
correlationId: payload.correlationId
|
|
208
|
-
}
|
|
209
|
-
});
|
|
152
|
+
this.clearActiveRun();
|
|
153
|
+
this.setError(null);
|
|
210
154
|
if (this.streamingMessage && (!targetMessageId || this.streamingMessage.id === targetMessageId)) {
|
|
155
|
+
const streamingMessageId = this.streamingMessage.id;
|
|
211
156
|
this.upsertMessage({
|
|
212
157
|
...this.streamingMessage,
|
|
213
|
-
status: "
|
|
158
|
+
status: "final"
|
|
214
159
|
});
|
|
215
160
|
this.replaceStreamingMessage(null);
|
|
216
161
|
if (targetMessageId) {
|
|
217
162
|
this.clearToolCallTrackingByMessageId(targetMessageId);
|
|
163
|
+
} else {
|
|
164
|
+
this.clearToolCallTrackingByMessageId(streamingMessageId);
|
|
218
165
|
}
|
|
219
166
|
}
|
|
220
167
|
}
|
|
@@ -264,15 +211,12 @@ var DefaultNcpAgentConversationStateManager = class {
|
|
|
264
211
|
}
|
|
265
212
|
const targetMessage = this.ensureStreamingMessage(payload.sessionId, payload.messageId, "streaming");
|
|
266
213
|
const nextParts = [...targetMessage.parts];
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
text: `${existingPart.text}${payload.delta}`
|
|
274
|
-
};
|
|
275
|
-
}
|
|
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
|
+
};
|
|
276
220
|
} else {
|
|
277
221
|
nextParts.push({ type: "reasoning", text: payload.delta });
|
|
278
222
|
}
|
|
@@ -371,13 +315,30 @@ var DefaultNcpAgentConversationStateManager = class {
|
|
|
371
315
|
this.stateVersion += 1;
|
|
372
316
|
}
|
|
373
317
|
handleRunFinished(_payload) {
|
|
374
|
-
this.
|
|
375
|
-
|
|
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();
|
|
376
329
|
}
|
|
377
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
|
+
}
|
|
378
340
|
this.setError(buildRuntimeError(payload));
|
|
379
|
-
this.
|
|
380
|
-
this.stateVersion += 1;
|
|
341
|
+
this.clearActiveRun();
|
|
381
342
|
}
|
|
382
343
|
handleRunMetadata(payload) {
|
|
383
344
|
const m = payload.metadata;
|
|
@@ -390,8 +351,7 @@ var DefaultNcpAgentConversationStateManager = class {
|
|
|
390
351
|
};
|
|
391
352
|
this.stateVersion += 1;
|
|
392
353
|
} else if (m?.kind === "final") {
|
|
393
|
-
this.
|
|
394
|
-
this.stateVersion += 1;
|
|
354
|
+
this.clearActiveRun();
|
|
395
355
|
}
|
|
396
356
|
}
|
|
397
357
|
handleEndpointError(payload) {
|
|
@@ -425,6 +385,21 @@ var DefaultNcpAgentConversationStateManager = class {
|
|
|
425
385
|
this.replaceStreamingMessage(nextStreamingMessage2);
|
|
426
386
|
return nextStreamingMessage2;
|
|
427
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
|
+
}
|
|
428
403
|
const nextStreamingMessage = {
|
|
429
404
|
id: messageId,
|
|
430
405
|
sessionId,
|
|
@@ -495,14 +470,6 @@ var DefaultNcpAgentConversationStateManager = class {
|
|
|
495
470
|
nextParts.push(toolPart);
|
|
496
471
|
return nextParts;
|
|
497
472
|
}
|
|
498
|
-
findLastReasoningPartIndex(parts) {
|
|
499
|
-
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
|
500
|
-
if (parts[index]?.type === "reasoning") {
|
|
501
|
-
return index;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
return -1;
|
|
505
|
-
}
|
|
506
473
|
upsertMessage(message) {
|
|
507
474
|
const normalizedMessage = cloneMessage(message);
|
|
508
475
|
const messageIndex = this.messages.findIndex((item) => item.id === normalizedMessage.id);
|
|
@@ -531,6 +498,13 @@ var DefaultNcpAgentConversationStateManager = class {
|
|
|
531
498
|
this.error = nextError ? { ...nextError, details: nextError.details ? { ...nextError.details } : void 0 } : null;
|
|
532
499
|
this.stateVersion += 1;
|
|
533
500
|
}
|
|
501
|
+
clearActiveRun() {
|
|
502
|
+
if (!this.activeRun) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
this.activeRun = null;
|
|
506
|
+
this.stateVersion += 1;
|
|
507
|
+
}
|
|
534
508
|
clearToolCallTrackingByMessageId(messageId) {
|
|
535
509
|
for (const [toolCallId, trackedMessageId] of this.toolCallMessageIdByCallId) {
|
|
536
510
|
if (trackedMessageId !== messageId) {
|
|
@@ -540,7 +514,638 @@ var DefaultNcpAgentConversationStateManager = class {
|
|
|
540
514
|
this.toolCallArgsRawByCallId.delete(toolCallId);
|
|
541
515
|
}
|
|
542
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
|
+
}
|
|
543
1142
|
};
|
|
544
1143
|
export {
|
|
545
|
-
|
|
1144
|
+
AgentRunExecutor,
|
|
1145
|
+
DefaultNcpAgentBackend,
|
|
1146
|
+
DefaultNcpAgentConversationStateManager,
|
|
1147
|
+
EventPublisher,
|
|
1148
|
+
InMemoryAgentSessionStore,
|
|
1149
|
+
NcpErrorException,
|
|
1150
|
+
createAgentClientFromServer
|
|
546
1151
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ncp-toolkit",
|
|
3
|
-
"version": "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.
|
|
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",
|