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