@neta-art/cohub 1.5.1 → 1.7.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 +21 -0
- package/dist/apis/prompts.d.ts +3 -4
- package/dist/apis/prompts.js +7 -13
- package/dist/apis/spaces.d.ts +24 -1
- package/dist/apis/spaces.js +25 -0
- package/dist/client.js +1 -1
- package/dist/http.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/session-generation-stream.d.ts +114 -0
- package/dist/session-generation-stream.js +513 -0
- package/dist/session-patch-reducer.d.ts +2 -1
- package/dist/session-patch-reducer.js +37 -3
- package/dist/types.d.ts +15 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -120,6 +120,27 @@ space.on("message.persisted", (event) => {
|
|
|
120
120
|
});
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
For product UIs, prefer the normalized generation stream. It folds
|
|
124
|
+
`session.turn.snapshot`, `session.turn.patch`, legacy `session.turn.progress`,
|
|
125
|
+
persisted assistant messages, finalized turns, and turn errors into stable
|
|
126
|
+
semantic events.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const stop = session.subscribeGeneration({
|
|
130
|
+
state(event) {
|
|
131
|
+
console.log(event.source, event.state.contentBlocks);
|
|
132
|
+
},
|
|
133
|
+
commit(event) {
|
|
134
|
+
if (event.commit.kind === "final") {
|
|
135
|
+
console.log("assistant final", event.commit.message);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
outOfSync() {
|
|
139
|
+
// Fetch `session.turns.streamSnapshot()` or reconcile persisted turns.
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
123
144
|
Supported business event names:
|
|
124
145
|
|
|
125
146
|
- `turn.patch`
|
package/dist/apis/prompts.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type { Fetch } from "../transport.js";
|
|
1
|
+
import type { HttpTransport, Fetch } from "../transport.js";
|
|
2
2
|
import type { PromptTemplateCatalogResponse } from "../types.js";
|
|
3
3
|
export declare class PromptsApi {
|
|
4
|
-
private readonly
|
|
5
|
-
|
|
6
|
-
constructor(fetcher: Fetch, baseUrl: string);
|
|
4
|
+
private readonly transport;
|
|
5
|
+
constructor(transport: HttpTransport);
|
|
7
6
|
list(options?: {
|
|
8
7
|
spaceId?: string;
|
|
9
8
|
}, customFetch?: Fetch): Promise<PromptTemplateCatalogResponse>;
|
package/dist/apis/prompts.js
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
export class PromptsApi {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
this.fetcher = fetcher;
|
|
6
|
-
this.baseUrl = baseUrl;
|
|
2
|
+
transport;
|
|
3
|
+
constructor(transport) {
|
|
4
|
+
this.transport = transport;
|
|
7
5
|
}
|
|
8
6
|
async list(options, customFetch) {
|
|
9
|
-
const fetchImpl = customFetch ?? this.fetcher;
|
|
10
7
|
const params = new URLSearchParams();
|
|
11
8
|
if (options?.spaceId)
|
|
12
9
|
params.set("spaceId", options.spaceId);
|
|
13
10
|
const query = params.toString();
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
throw new Error(`Failed to fetch prompt templates: ${response.status} ${response.statusText}`);
|
|
19
|
-
}
|
|
20
|
-
return response.json();
|
|
11
|
+
const path = query ? `/api/prompts?${query}` : "/api/prompts";
|
|
12
|
+
return this.transport.request(path, {
|
|
13
|
+
fetch: customFetch,
|
|
14
|
+
});
|
|
21
15
|
}
|
|
22
16
|
}
|
package/dist/apis/spaces.d.ts
CHANGED
|
@@ -2,13 +2,21 @@ import type { SpacePublicEndpoints } from "@neta-art/cohub-protocol/ports";
|
|
|
2
2
|
import type { WebsocketClient, WebsocketEventPayload } from "../websocket.js";
|
|
3
3
|
import type { HttpTransport, Fetch } from "../transport.js";
|
|
4
4
|
import { type SessionPatchApplyResult } from "../session-patch-reducer.js";
|
|
5
|
-
import
|
|
5
|
+
import { SessionGenerationStreamClient, type GenerationStreamSubscriptionHandlers } from "../session-generation-stream.js";
|
|
6
|
+
import type { CheckpointRecord, ContentBlock, SessionForkRecord, SessionMessageResponse, SessionMessagesPaginatedResponse, SessionMessagesResponse, SessionTurnResponse, SessionTurnStreamSnapshotResponse, SessionTurnIndexResponse, SessionTurnWindowResponse, SessionTurnsPaginatedResponse, SessionTurnSignedUrlsResponse, SessionRecord, SpaceAccessPolicy, SpaceBootstrapSource, SpaceChannelBindingInput, SpaceCheckpointDetailResponse, SpaceCreateResponse, CreateSpacePromptInput, CreateSpacePromptResponse, SpaceEnvInput, SpaceFsCompleteUploadInput, SpaceFsCompleteUploadResponse, SpaceFsCreateUploadInput, SpaceFsCreateUploadResponse, SpaceFsFileResponse, SpaceFsMoveInput, SpaceFsTreeResponse, SpaceFsUploadResponse, SpaceUsageResponse, SpaceFsWriteFileInput, SpaceMarkKind, SpaceMarkListItem, SpaceMarkResourceType, SpaceMember, SpaceRecord, SpaceRole, SpaceSessionsResponse } from "../types.js";
|
|
6
7
|
import { SpaceInvitationsApi } from "./invitations.js";
|
|
7
8
|
export type SessionSubscriptionHandlers = {
|
|
8
9
|
patch?: (event: WebsocketEventPayload) => void;
|
|
10
|
+
/**
|
|
11
|
+
* @deprecated Use `session.subscribeGeneration({ state })` for normalized
|
|
12
|
+
* snapshot/patch/progress generation state.
|
|
13
|
+
*/
|
|
9
14
|
patchState?: (result: SessionPatchApplyResult) => void;
|
|
15
|
+
/** @deprecated Use `session.subscribeGeneration({ state })`. */
|
|
10
16
|
snapshot?: (event: WebsocketEventPayload) => void;
|
|
17
|
+
/** @deprecated Legacy progress events are normalized by `subscribeGeneration`. */
|
|
11
18
|
progress?: (event: WebsocketEventPayload) => void;
|
|
19
|
+
/** @deprecated Use `session.subscribeGeneration({ commit, finalized })`. */
|
|
12
20
|
final?: (event: WebsocketEventPayload) => void;
|
|
13
21
|
turnUpdated?: (event: WebsocketEventPayload) => void;
|
|
14
22
|
turnFinalized?: (event: WebsocketEventPayload) => void;
|
|
@@ -140,6 +148,7 @@ export declare class SessionClient {
|
|
|
140
148
|
readonly messages: SessionMessagesClient;
|
|
141
149
|
readonly turns: SessionTurnsClient;
|
|
142
150
|
readonly realtime: SessionRealtimeClient;
|
|
151
|
+
readonly generation: SessionGenerationStreamClient;
|
|
143
152
|
constructor(spaceId: string, id: string, transport: HttpTransport, websocketClient: WebsocketClient | null);
|
|
144
153
|
get(customFetch?: Fetch): Promise<{
|
|
145
154
|
space: SpaceRecord;
|
|
@@ -148,7 +157,21 @@ export declare class SessionClient {
|
|
|
148
157
|
rename(title: string | null, customFetch?: Fetch): Promise<{
|
|
149
158
|
session: SessionRecord;
|
|
150
159
|
}>;
|
|
160
|
+
abort(optionsOrFetch?: {
|
|
161
|
+
turnId?: string | null;
|
|
162
|
+
} | Fetch, customFetch?: Fetch): Promise<{
|
|
163
|
+
ok: true;
|
|
164
|
+
}>;
|
|
165
|
+
turn(turnId: string): {
|
|
166
|
+
fork: (input?: {
|
|
167
|
+
title?: string | null;
|
|
168
|
+
}) => Promise<{
|
|
169
|
+
session: SessionRecord;
|
|
170
|
+
fork: SessionForkRecord;
|
|
171
|
+
}>;
|
|
172
|
+
};
|
|
151
173
|
subscribe(handlers: SessionSubscriptionHandlers): () => void;
|
|
174
|
+
subscribeGeneration(handlers: GenerationStreamSubscriptionHandlers): () => void;
|
|
152
175
|
on(type: SessionEventName, handler: (event: WebsocketEventPayload) => void): () => void;
|
|
153
176
|
}
|
|
154
177
|
export declare class SpaceSessionsApi {
|
package/dist/apis/spaces.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ensureRealtimeConnected } from "../realtime.js";
|
|
2
2
|
import { SessionPatchReducer, } from "../session-patch-reducer.js";
|
|
3
|
+
import { SessionGenerationStreamClient, } from "../session-generation-stream.js";
|
|
3
4
|
import { SpaceInvitationsApi } from "./invitations.js";
|
|
4
5
|
const DEFAULT_DEDUP_WINDOW_MS = 2000;
|
|
5
6
|
const getFilenameFromContentDisposition = (value) => {
|
|
@@ -405,6 +406,7 @@ export class SessionClient {
|
|
|
405
406
|
messages;
|
|
406
407
|
turns;
|
|
407
408
|
realtime;
|
|
409
|
+
generation;
|
|
408
410
|
constructor(spaceId, id, transport, websocketClient) {
|
|
409
411
|
this.spaceId = spaceId;
|
|
410
412
|
this.id = id;
|
|
@@ -412,6 +414,7 @@ export class SessionClient {
|
|
|
412
414
|
this.messages = new SessionMessagesClient(transport, id);
|
|
413
415
|
this.turns = new SessionTurnsClient(transport, id);
|
|
414
416
|
this.realtime = new SessionRealtimeClient(websocketClient, spaceId, id);
|
|
417
|
+
this.generation = new SessionGenerationStreamClient(websocketClient, spaceId, id);
|
|
415
418
|
}
|
|
416
419
|
get(customFetch) {
|
|
417
420
|
return this.transport.request(`/api/sessions/${this.id}`, {
|
|
@@ -425,9 +428,31 @@ export class SessionClient {
|
|
|
425
428
|
fetch: customFetch,
|
|
426
429
|
});
|
|
427
430
|
}
|
|
431
|
+
abort(optionsOrFetch, customFetch) {
|
|
432
|
+
const options = typeof optionsOrFetch === "function" ? undefined : optionsOrFetch;
|
|
433
|
+
const fetch = typeof optionsOrFetch === "function" ? optionsOrFetch : customFetch;
|
|
434
|
+
return this.transport.request(`/api/sessions/${this.id}/abort`, {
|
|
435
|
+
method: "POST",
|
|
436
|
+
headers: { "Content-Type": "application/json" },
|
|
437
|
+
body: JSON.stringify({ turnId: options?.turnId ?? null }),
|
|
438
|
+
fetch,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
turn(turnId) {
|
|
442
|
+
return {
|
|
443
|
+
fork: (input = {}) => this.transport.request(`/api/sessions/${this.id}/turns/${turnId}/fork`, {
|
|
444
|
+
method: "POST",
|
|
445
|
+
headers: { "Content-Type": "application/json" },
|
|
446
|
+
body: JSON.stringify(input),
|
|
447
|
+
}),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
428
450
|
subscribe(handlers) {
|
|
429
451
|
return this.realtime.subscribe(handlers);
|
|
430
452
|
}
|
|
453
|
+
subscribeGeneration(handlers) {
|
|
454
|
+
return this.generation.subscribe(handlers);
|
|
455
|
+
}
|
|
431
456
|
on(type, handler) {
|
|
432
457
|
return this.realtime.on(type, handler);
|
|
433
458
|
}
|
package/dist/client.js
CHANGED
|
@@ -39,7 +39,7 @@ export class CohubClient {
|
|
|
39
39
|
this.channels = new ChannelsApi(this.transport);
|
|
40
40
|
this.user = new UserApi(this.transport, apiBaseUrl, options.setStoredAuthToken, options.clearStoredAuthToken);
|
|
41
41
|
this.models = new ModelsApi(this.transport);
|
|
42
|
-
this.prompts = new PromptsApi(
|
|
42
|
+
this.prompts = new PromptsApi(this.transport);
|
|
43
43
|
this.sessionAccess = new SessionAccessApi(this.transport);
|
|
44
44
|
this.tasks = new TasksApi(this.transport);
|
|
45
45
|
this.cronJobs = new CronJobsApi(this.transport);
|
package/dist/http.js
CHANGED
|
@@ -27,7 +27,7 @@ export class CohubHttpClient {
|
|
|
27
27
|
this.channels = new ChannelsApi(this.transport);
|
|
28
28
|
this.user = new UserApi(this.transport, apiBaseUrl, options.setStoredAuthToken, options.clearStoredAuthToken);
|
|
29
29
|
this.models = new ModelsApi(this.transport);
|
|
30
|
-
this.prompts = new PromptsApi(
|
|
30
|
+
this.prompts = new PromptsApi(this.transport);
|
|
31
31
|
this.sessionAccess = new SessionAccessApi(this.transport);
|
|
32
32
|
this.tasks = new TasksApi(this.transport);
|
|
33
33
|
this.cronJobs = new CronJobsApi(this.transport);
|
package/dist/index.d.ts
CHANGED
|
@@ -2,11 +2,13 @@ export { CohubHttpClient, createHttpClient } from "./http.js";
|
|
|
2
2
|
export { CohubClient, createCohubClient } from "./client.js";
|
|
3
3
|
export { WebsocketClient, createWebsocketClient } from "./websocket.js";
|
|
4
4
|
export { SessionPatchReducer, createSessionPatchReducer } from "./session-patch-reducer.js";
|
|
5
|
+
export { SessionGenerationStreamClient, createSessionGenerationStreamClient, parseAssistantMessageCommit, } from "./session-generation-stream.js";
|
|
5
6
|
export { HttpError } from "./transport.js";
|
|
6
7
|
export type { RawHttpResponse } from "./transport.js";
|
|
7
8
|
export { COHUB_ENVIRONMENTS, normalizeBaseUrl, normalizeWebsocketUrl, resolveApiBaseUrl, resolveCohubEnvironment, resolveWebsocketUrl, } from "./environment.js";
|
|
8
9
|
export type { CohubClientOptions, Fetch } from "./transport.js";
|
|
9
10
|
export type { CohubEnvironment } from "./environment.js";
|
|
10
11
|
export type { SessionPatchApplyInput, SessionPatchApplyResult, SessionPatchState, SessionPatchStatus, } from "./session-patch-reducer.js";
|
|
12
|
+
export type { AssistantMessageCommit, GenerationStreamCommitEvent, GenerationStreamErrorEvent, GenerationStreamEvent, GenerationStreamFinalizedEvent, GenerationStreamIntermediateMessage, GenerationStreamOutOfSyncEvent, GenerationStreamStateEvent, GenerationStreamSubscriptionHandlers, GenerationStreamTurnUpdatedEvent, } from "./session-generation-stream.js";
|
|
11
13
|
export * from "./types.js";
|
|
12
14
|
export type { SessionEventName, SessionSubscriptionHandlers, SpaceChannelBindingRecord, SpaceEventName, WebSocketConnectionState } from "./apis/spaces.js";
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { CohubHttpClient, createHttpClient } from "./http.js";
|
|
|
2
2
|
export { CohubClient, createCohubClient } from "./client.js";
|
|
3
3
|
export { WebsocketClient, createWebsocketClient } from "./websocket.js";
|
|
4
4
|
export { SessionPatchReducer, createSessionPatchReducer } from "./session-patch-reducer.js";
|
|
5
|
+
export { SessionGenerationStreamClient, createSessionGenerationStreamClient, parseAssistantMessageCommit, } from "./session-generation-stream.js";
|
|
5
6
|
export { HttpError } from "./transport.js";
|
|
6
7
|
export { COHUB_ENVIRONMENTS, normalizeBaseUrl, normalizeWebsocketUrl, resolveApiBaseUrl, resolveCohubEnvironment, resolveWebsocketUrl, } from "./environment.js";
|
|
7
8
|
export * from "./types.js";
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ContentBlock, Usage } from "@neta-art/cohub-protocol/core";
|
|
2
|
+
import type { MessageRecord, SessionTurnRecord } from "@neta-art/cohub-protocol/model";
|
|
3
|
+
import { type SessionPatchState } from "./session-patch-reducer.js";
|
|
4
|
+
import type { WebsocketClient, WebsocketEventPayload } from "./websocket.js";
|
|
5
|
+
export type AssistantMessageCommit = {
|
|
6
|
+
kind: "intermediate";
|
|
7
|
+
message: MessageRecord;
|
|
8
|
+
isFinal: false;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "final";
|
|
11
|
+
message: MessageRecord;
|
|
12
|
+
isFinal: true;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "error";
|
|
15
|
+
message: MessageRecord;
|
|
16
|
+
isFinal: true;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "ignored";
|
|
19
|
+
message: MessageRecord;
|
|
20
|
+
isFinal: false;
|
|
21
|
+
};
|
|
22
|
+
export type GenerationStreamIntermediateMessage = {
|
|
23
|
+
id?: string;
|
|
24
|
+
sessionId?: string;
|
|
25
|
+
role?: "user" | "assistant" | "system";
|
|
26
|
+
messageId: string | null;
|
|
27
|
+
messageOrdinal: number | null;
|
|
28
|
+
content: ContentBlock[];
|
|
29
|
+
text?: string | null;
|
|
30
|
+
provider?: string | null;
|
|
31
|
+
model?: string | null;
|
|
32
|
+
stopReason?: string | null;
|
|
33
|
+
errorMessage?: string | null;
|
|
34
|
+
usage?: Usage | null;
|
|
35
|
+
toolCallsObjectKey?: string | null;
|
|
36
|
+
meta?: Record<string, unknown> | null;
|
|
37
|
+
createdAt?: string;
|
|
38
|
+
};
|
|
39
|
+
export type GenerationStreamStateEvent = {
|
|
40
|
+
type: "state";
|
|
41
|
+
source: "snapshot" | "patch" | "progress";
|
|
42
|
+
state: SessionPatchState;
|
|
43
|
+
messageId: string | null;
|
|
44
|
+
messageOrdinal: number | null;
|
|
45
|
+
intermediateMessages: GenerationStreamIntermediateMessage[];
|
|
46
|
+
rawEvent: WebsocketEventPayload;
|
|
47
|
+
};
|
|
48
|
+
export type GenerationStreamCommitEvent = {
|
|
49
|
+
type: "commit";
|
|
50
|
+
commit: AssistantMessageCommit;
|
|
51
|
+
rawEvent: WebsocketEventPayload;
|
|
52
|
+
};
|
|
53
|
+
export type GenerationStreamFinalizedEvent = {
|
|
54
|
+
type: "finalized";
|
|
55
|
+
turn: SessionTurnRecord;
|
|
56
|
+
rawEvent: WebsocketEventPayload;
|
|
57
|
+
};
|
|
58
|
+
export type GenerationStreamTurnUpdatedEvent = {
|
|
59
|
+
type: "turn_updated";
|
|
60
|
+
turn: Partial<SessionTurnRecord>;
|
|
61
|
+
rawEvent: WebsocketEventPayload;
|
|
62
|
+
};
|
|
63
|
+
export type GenerationStreamErrorEvent = {
|
|
64
|
+
type: "error";
|
|
65
|
+
message: string;
|
|
66
|
+
rawEvent: WebsocketEventPayload;
|
|
67
|
+
};
|
|
68
|
+
export type GenerationStreamOutOfSyncEvent = {
|
|
69
|
+
type: "out_of_sync";
|
|
70
|
+
source: "snapshot" | "patch";
|
|
71
|
+
reason: "duplicate" | "version_mismatch" | "invalid";
|
|
72
|
+
state: SessionPatchState;
|
|
73
|
+
rawEvent: WebsocketEventPayload;
|
|
74
|
+
};
|
|
75
|
+
export type GenerationStreamEvent = GenerationStreamStateEvent | GenerationStreamCommitEvent | GenerationStreamFinalizedEvent | GenerationStreamTurnUpdatedEvent | GenerationStreamErrorEvent | GenerationStreamOutOfSyncEvent;
|
|
76
|
+
export type GenerationStreamSubscriptionHandlers = {
|
|
77
|
+
event?: (event: GenerationStreamEvent) => void;
|
|
78
|
+
state?: (event: GenerationStreamStateEvent) => void;
|
|
79
|
+
commit?: (event: GenerationStreamCommitEvent) => void;
|
|
80
|
+
finalized?: (event: GenerationStreamFinalizedEvent) => void;
|
|
81
|
+
turnUpdated?: (event: GenerationStreamTurnUpdatedEvent) => void;
|
|
82
|
+
error?: (event: GenerationStreamErrorEvent) => void;
|
|
83
|
+
outOfSync?: (event: GenerationStreamOutOfSyncEvent) => void;
|
|
84
|
+
};
|
|
85
|
+
export declare function parseAssistantMessageCommit(message: MessageRecord): AssistantMessageCommit;
|
|
86
|
+
export declare class SessionGenerationStreamClient {
|
|
87
|
+
private readonly websocketClient;
|
|
88
|
+
private readonly spaceId;
|
|
89
|
+
private readonly sessionId;
|
|
90
|
+
private readonly reducer;
|
|
91
|
+
private messageId;
|
|
92
|
+
private messageOrdinal;
|
|
93
|
+
private intermediateMessages;
|
|
94
|
+
private progressState;
|
|
95
|
+
constructor(websocketClient: WebsocketClient | null, spaceId: string, sessionId: string);
|
|
96
|
+
subscribe(handlers: GenerationStreamSubscriptionHandlers): () => void;
|
|
97
|
+
private emit;
|
|
98
|
+
private resetCurrentMessage;
|
|
99
|
+
private appendCurrentMessage;
|
|
100
|
+
private addIntermediateMessage;
|
|
101
|
+
private handleAppliedState;
|
|
102
|
+
private prepareMessageBoundary;
|
|
103
|
+
private handleSnapshot;
|
|
104
|
+
private handlePatch;
|
|
105
|
+
private handleProgress;
|
|
106
|
+
private handlePersisted;
|
|
107
|
+
private handleFinalized;
|
|
108
|
+
private handleEvent;
|
|
109
|
+
}
|
|
110
|
+
export declare function createSessionGenerationStreamClient(input: {
|
|
111
|
+
websocketClient: WebsocketClient | null;
|
|
112
|
+
spaceId: string;
|
|
113
|
+
sessionId: string;
|
|
114
|
+
}): SessionGenerationStreamClient;
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { ensureRealtimeConnected } from "./realtime.js";
|
|
2
|
+
import { SessionPatchReducer, } from "./session-patch-reducer.js";
|
|
3
|
+
const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
4
|
+
const isContentBlockArray = (value) => Array.isArray(value) &&
|
|
5
|
+
value.every((item) => isRecord(item) && typeof item.type === "string");
|
|
6
|
+
const stringField = (record, key) => {
|
|
7
|
+
const value = record[key];
|
|
8
|
+
return typeof value === "string" ? value : null;
|
|
9
|
+
};
|
|
10
|
+
const numberField = (record, key) => {
|
|
11
|
+
const value = record[key];
|
|
12
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
13
|
+
};
|
|
14
|
+
const getMessageKind = (message) => {
|
|
15
|
+
const kind = message.meta?.messageKind;
|
|
16
|
+
return typeof kind === "string" ? kind : null;
|
|
17
|
+
};
|
|
18
|
+
export function parseAssistantMessageCommit(message) {
|
|
19
|
+
if (message.role !== "assistant") {
|
|
20
|
+
return { kind: "ignored", message, isFinal: false };
|
|
21
|
+
}
|
|
22
|
+
const kind = getMessageKind(message);
|
|
23
|
+
if (kind === "assistant_intermediate") {
|
|
24
|
+
return { kind: "intermediate", message, isFinal: false };
|
|
25
|
+
}
|
|
26
|
+
if (kind === "assistant_final") {
|
|
27
|
+
return { kind: "final", message, isFinal: true };
|
|
28
|
+
}
|
|
29
|
+
if (kind === "assistant_error") {
|
|
30
|
+
return { kind: "error", message, isFinal: true };
|
|
31
|
+
}
|
|
32
|
+
return { kind: "ignored", message, isFinal: false };
|
|
33
|
+
}
|
|
34
|
+
function cloneContentBlock(block) {
|
|
35
|
+
return structuredClone(block);
|
|
36
|
+
}
|
|
37
|
+
function getStreamIndex(block) {
|
|
38
|
+
const value = block._meta?.streamIndex;
|
|
39
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
40
|
+
}
|
|
41
|
+
function findMergeTargetIndex(result, block) {
|
|
42
|
+
const streamIndex = getStreamIndex(block);
|
|
43
|
+
if (streamIndex != null) {
|
|
44
|
+
return result.findIndex((existing) => existing.type === block.type && getStreamIndex(existing) === streamIndex);
|
|
45
|
+
}
|
|
46
|
+
if (block.type === "tool_use") {
|
|
47
|
+
return result.findIndex((existing) => existing.type === "tool_use" && existing.id === block.id);
|
|
48
|
+
}
|
|
49
|
+
if (block.type === "tool_result") {
|
|
50
|
+
return result.findIndex((existing) => existing.type === "tool_result" &&
|
|
51
|
+
existing.tool_use_id === block.tool_use_id);
|
|
52
|
+
}
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
function mergeStreamingDeltaBlocks(existing, delta) {
|
|
56
|
+
if (delta.length === 0)
|
|
57
|
+
return existing;
|
|
58
|
+
const result = existing.map(cloneContentBlock);
|
|
59
|
+
for (const block of delta) {
|
|
60
|
+
const targetIndex = findMergeTargetIndex(result, block);
|
|
61
|
+
if (targetIndex === -1) {
|
|
62
|
+
result.push(cloneContentBlock(block));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const target = result[targetIndex];
|
|
66
|
+
if (block.type === "text" && target?.type === "text") {
|
|
67
|
+
target.text += block.text;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (block.type === "thinking" && target?.type === "thinking") {
|
|
71
|
+
target.thinking += block.thinking;
|
|
72
|
+
if (block.signature)
|
|
73
|
+
target.signature = block.signature;
|
|
74
|
+
if (block._meta)
|
|
75
|
+
target._meta = { ...(target._meta ?? {}), ...block._meta };
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
result[targetIndex] = Object.assign(target ?? {}, cloneContentBlock(block));
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
function parseSnapshotMessage(value) {
|
|
83
|
+
if (!isRecord(value) || !isContentBlockArray(value.content))
|
|
84
|
+
return null;
|
|
85
|
+
return {
|
|
86
|
+
messageId: stringField(value, "messageId"),
|
|
87
|
+
messageOrdinal: numberField(value, "messageOrdinal"),
|
|
88
|
+
content: value.content,
|
|
89
|
+
...(typeof value.id === "string" ? { id: value.id } : {}),
|
|
90
|
+
...(typeof value.sessionId === "string" ? { sessionId: value.sessionId } : {}),
|
|
91
|
+
...(value.role === "user" ||
|
|
92
|
+
value.role === "assistant" ||
|
|
93
|
+
value.role === "system"
|
|
94
|
+
? { role: value.role }
|
|
95
|
+
: {}),
|
|
96
|
+
...(typeof value.text === "string" ? { text: value.text } : {}),
|
|
97
|
+
...(typeof value.provider === "string" ? { provider: value.provider } : {}),
|
|
98
|
+
...(typeof value.model === "string" ? { model: value.model } : {}),
|
|
99
|
+
...(typeof value.stopReason === "string"
|
|
100
|
+
? { stopReason: value.stopReason }
|
|
101
|
+
: {}),
|
|
102
|
+
...(typeof value.errorMessage === "string"
|
|
103
|
+
? { errorMessage: value.errorMessage }
|
|
104
|
+
: {}),
|
|
105
|
+
...(isRecord(value.usage) ? { usage: value.usage } : {}),
|
|
106
|
+
...(typeof value.toolCallsObjectKey === "string"
|
|
107
|
+
? { toolCallsObjectKey: value.toolCallsObjectKey }
|
|
108
|
+
: {}),
|
|
109
|
+
...(isRecord(value.meta) ? { meta: value.meta } : {}),
|
|
110
|
+
...(typeof value.createdAt === "string" ? { createdAt: value.createdAt } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function messageRecordToIntermediate(message) {
|
|
114
|
+
if (!isContentBlockArray(message.content) || message.content.length === 0) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const meta = isRecord(message.meta) ? message.meta : {};
|
|
118
|
+
return {
|
|
119
|
+
id: message.id,
|
|
120
|
+
sessionId: message.sessionId,
|
|
121
|
+
role: message.role,
|
|
122
|
+
messageId: typeof meta.streamMessageId === "string"
|
|
123
|
+
? meta.streamMessageId
|
|
124
|
+
: message.id ?? null,
|
|
125
|
+
messageOrdinal: typeof meta.messageOrdinal === "number" ? meta.messageOrdinal : null,
|
|
126
|
+
content: message.content,
|
|
127
|
+
text: message.text,
|
|
128
|
+
provider: message.provider,
|
|
129
|
+
model: message.model,
|
|
130
|
+
stopReason: message.stopReason,
|
|
131
|
+
errorMessage: message.errorMessage,
|
|
132
|
+
usage: message.usage,
|
|
133
|
+
meta: message.meta,
|
|
134
|
+
createdAt: message.createdAt,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function resolveStreamMessageId(input) {
|
|
138
|
+
if (input.messageId?.trim())
|
|
139
|
+
return input.messageId.trim();
|
|
140
|
+
if (input.messageOrdinal == null)
|
|
141
|
+
return null;
|
|
142
|
+
if (input.turnId?.trim()) {
|
|
143
|
+
return `turn:${input.turnId.trim()}:assistant:${input.messageOrdinal}`;
|
|
144
|
+
}
|
|
145
|
+
return `session:${input.sessionId}:assistant:${input.messageOrdinal}:${input.anchorUserMessageId ?? "unknown"}`;
|
|
146
|
+
}
|
|
147
|
+
function getTurnIdFromMessage(message) {
|
|
148
|
+
const turnId = message.meta?.turnId;
|
|
149
|
+
return typeof turnId === "string" ? turnId : null;
|
|
150
|
+
}
|
|
151
|
+
export class SessionGenerationStreamClient {
|
|
152
|
+
websocketClient;
|
|
153
|
+
spaceId;
|
|
154
|
+
sessionId;
|
|
155
|
+
reducer = new SessionPatchReducer();
|
|
156
|
+
messageId = null;
|
|
157
|
+
messageOrdinal = null;
|
|
158
|
+
intermediateMessages = [];
|
|
159
|
+
progressState = null;
|
|
160
|
+
constructor(websocketClient, spaceId, sessionId) {
|
|
161
|
+
this.websocketClient = websocketClient;
|
|
162
|
+
this.spaceId = spaceId;
|
|
163
|
+
this.sessionId = sessionId;
|
|
164
|
+
}
|
|
165
|
+
subscribe(handlers) {
|
|
166
|
+
if (!this.websocketClient) {
|
|
167
|
+
throw new Error("realtime transport is not configured for this client");
|
|
168
|
+
}
|
|
169
|
+
ensureRealtimeConnected(this.websocketClient);
|
|
170
|
+
const unsubscribe = this.websocketClient.on("event", (event) => {
|
|
171
|
+
if (event.spaceId !== this.spaceId || event.sessionId !== this.sessionId) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this.handleEvent(event, handlers);
|
|
175
|
+
});
|
|
176
|
+
return () => unsubscribe();
|
|
177
|
+
}
|
|
178
|
+
emit(handlers, event) {
|
|
179
|
+
handlers.event?.(event);
|
|
180
|
+
if (event.type === "state")
|
|
181
|
+
handlers.state?.(event);
|
|
182
|
+
if (event.type === "commit")
|
|
183
|
+
handlers.commit?.(event);
|
|
184
|
+
if (event.type === "finalized")
|
|
185
|
+
handlers.finalized?.(event);
|
|
186
|
+
if (event.type === "turn_updated")
|
|
187
|
+
handlers.turnUpdated?.(event);
|
|
188
|
+
if (event.type === "error")
|
|
189
|
+
handlers.error?.(event);
|
|
190
|
+
if (event.type === "out_of_sync")
|
|
191
|
+
handlers.outOfSync?.(event);
|
|
192
|
+
}
|
|
193
|
+
resetCurrentMessage() {
|
|
194
|
+
this.messageId = null;
|
|
195
|
+
this.messageOrdinal = null;
|
|
196
|
+
this.progressState = null;
|
|
197
|
+
}
|
|
198
|
+
appendCurrentMessage(state) {
|
|
199
|
+
if (state.contentBlocks.length === 0)
|
|
200
|
+
return;
|
|
201
|
+
this.addIntermediateMessage({
|
|
202
|
+
messageId: this.messageId,
|
|
203
|
+
messageOrdinal: this.messageOrdinal,
|
|
204
|
+
content: state.contentBlocks,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
addIntermediateMessage(message) {
|
|
208
|
+
const index = this.intermediateMessages.findIndex((existing) => {
|
|
209
|
+
if (message.messageId && existing.messageId) {
|
|
210
|
+
return existing.messageId === message.messageId;
|
|
211
|
+
}
|
|
212
|
+
return (message.messageOrdinal !== null &&
|
|
213
|
+
existing.messageOrdinal === message.messageOrdinal);
|
|
214
|
+
});
|
|
215
|
+
if (index < 0) {
|
|
216
|
+
this.intermediateMessages = [...this.intermediateMessages, message];
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.intermediateMessages = this.intermediateMessages.map((existing, i) => i === index ? { ...existing, ...message } : existing);
|
|
220
|
+
}
|
|
221
|
+
handleAppliedState(handlers, source, result, rawEvent, messageId, messageOrdinal) {
|
|
222
|
+
if (!result.applied) {
|
|
223
|
+
this.emit(handlers, {
|
|
224
|
+
type: "out_of_sync",
|
|
225
|
+
source: source === "progress" ? "patch" : source,
|
|
226
|
+
reason: result.reason,
|
|
227
|
+
state: result.state,
|
|
228
|
+
rawEvent,
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.progressState = result.state;
|
|
233
|
+
this.messageId = messageId;
|
|
234
|
+
this.messageOrdinal = messageOrdinal;
|
|
235
|
+
this.emit(handlers, {
|
|
236
|
+
type: "state",
|
|
237
|
+
source,
|
|
238
|
+
state: result.state,
|
|
239
|
+
messageId,
|
|
240
|
+
messageOrdinal,
|
|
241
|
+
intermediateMessages: [...this.intermediateMessages],
|
|
242
|
+
rawEvent,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
prepareMessageBoundary(input) {
|
|
246
|
+
const current = this.progressState ??
|
|
247
|
+
this.reducer.get({
|
|
248
|
+
spaceId: this.spaceId,
|
|
249
|
+
sessionId: this.sessionId,
|
|
250
|
+
});
|
|
251
|
+
const nextMessageId = resolveStreamMessageId({
|
|
252
|
+
sessionId: this.sessionId,
|
|
253
|
+
turnId: input.turnId,
|
|
254
|
+
anchorUserMessageId: input.anchorUserMessageId,
|
|
255
|
+
messageId: input.messageId,
|
|
256
|
+
messageOrdinal: input.messageOrdinal,
|
|
257
|
+
});
|
|
258
|
+
const differentTurn = Boolean(current.turnId && input.turnId && current.turnId !== input.turnId);
|
|
259
|
+
const messageChanged = Boolean(nextMessageId &&
|
|
260
|
+
current.contentBlocks.length > 0 &&
|
|
261
|
+
this.messageId &&
|
|
262
|
+
nextMessageId !== this.messageId);
|
|
263
|
+
if (differentTurn) {
|
|
264
|
+
this.intermediateMessages = [];
|
|
265
|
+
this.resetCurrentMessage();
|
|
266
|
+
this.reducer.start({
|
|
267
|
+
spaceId: this.spaceId,
|
|
268
|
+
sessionId: this.sessionId,
|
|
269
|
+
turnId: input.turnId,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
else if (messageChanged) {
|
|
273
|
+
this.appendCurrentMessage(current);
|
|
274
|
+
this.resetCurrentMessage();
|
|
275
|
+
this.reducer.start({
|
|
276
|
+
spaceId: this.spaceId,
|
|
277
|
+
sessionId: this.sessionId,
|
|
278
|
+
turnId: input.turnId ?? current.turnId,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return nextMessageId;
|
|
282
|
+
}
|
|
283
|
+
handleSnapshot(event, handlers) {
|
|
284
|
+
const payload = event.payload;
|
|
285
|
+
const current = isRecord(payload.current) ? payload.current : null;
|
|
286
|
+
const content = current ? current.content : null;
|
|
287
|
+
const seq = typeof payload.seq === "number" ? payload.seq : null;
|
|
288
|
+
if (!current || !isContentBlockArray(content) || seq === null) {
|
|
289
|
+
this.emit(handlers, {
|
|
290
|
+
type: "out_of_sync",
|
|
291
|
+
source: "snapshot",
|
|
292
|
+
reason: "invalid",
|
|
293
|
+
state: this.reducer.get({ spaceId: this.spaceId, sessionId: this.sessionId }),
|
|
294
|
+
rawEvent: event,
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const turnId = typeof payload.turnId === "string" ? payload.turnId : null;
|
|
299
|
+
const anchorUserMessageId = typeof payload.anchorUserMessageId === "string"
|
|
300
|
+
? payload.anchorUserMessageId
|
|
301
|
+
: null;
|
|
302
|
+
const messageOrdinal = numberField(current, "messageOrdinal");
|
|
303
|
+
const messageId = this.prepareMessageBoundary({
|
|
304
|
+
turnId,
|
|
305
|
+
messageId: stringField(current, "messageId"),
|
|
306
|
+
messageOrdinal,
|
|
307
|
+
anchorUserMessageId,
|
|
308
|
+
});
|
|
309
|
+
this.intermediateMessages = Array.isArray(payload.intermediateMessages)
|
|
310
|
+
? payload.intermediateMessages
|
|
311
|
+
.map(parseSnapshotMessage)
|
|
312
|
+
.filter((message) => message !== null)
|
|
313
|
+
: [];
|
|
314
|
+
const result = this.reducer.applySnapshot({
|
|
315
|
+
spaceId: this.spaceId,
|
|
316
|
+
sessionId: this.sessionId,
|
|
317
|
+
turnId,
|
|
318
|
+
seq,
|
|
319
|
+
contentBlocks: content,
|
|
320
|
+
anchorUserMessageId,
|
|
321
|
+
appendPath: stringField(current, "appendPath"),
|
|
322
|
+
});
|
|
323
|
+
this.handleAppliedState(handlers, "snapshot", result, event, messageId, messageOrdinal);
|
|
324
|
+
}
|
|
325
|
+
handlePatch(event, handlers) {
|
|
326
|
+
const payload = event.payload;
|
|
327
|
+
const seq = typeof payload.seq === "number" ? payload.seq : null;
|
|
328
|
+
const baseSeq = typeof payload.baseSeq === "number" ? payload.baseSeq : null;
|
|
329
|
+
if (seq === null || baseSeq === null || !Array.isArray(payload.ops)) {
|
|
330
|
+
this.emit(handlers, {
|
|
331
|
+
type: "out_of_sync",
|
|
332
|
+
source: "patch",
|
|
333
|
+
reason: "invalid",
|
|
334
|
+
state: this.reducer.get({ spaceId: this.spaceId, sessionId: this.sessionId }),
|
|
335
|
+
rawEvent: event,
|
|
336
|
+
});
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const turnId = typeof payload.turnId === "string" ? payload.turnId : null;
|
|
340
|
+
const anchorUserMessageId = typeof payload.anchorUserMessageId === "string"
|
|
341
|
+
? payload.anchorUserMessageId
|
|
342
|
+
: null;
|
|
343
|
+
const messageOrdinal = typeof payload.messageOrdinal === "number" ? payload.messageOrdinal : null;
|
|
344
|
+
const messageId = this.prepareMessageBoundary({
|
|
345
|
+
turnId,
|
|
346
|
+
messageId: typeof payload.messageId === "string" ? payload.messageId : null,
|
|
347
|
+
messageOrdinal,
|
|
348
|
+
anchorUserMessageId,
|
|
349
|
+
});
|
|
350
|
+
const input = {
|
|
351
|
+
spaceId: this.spaceId,
|
|
352
|
+
sessionId: this.sessionId,
|
|
353
|
+
turnId,
|
|
354
|
+
seq,
|
|
355
|
+
baseSeq,
|
|
356
|
+
ops: payload.ops,
|
|
357
|
+
anchorUserMessageId,
|
|
358
|
+
};
|
|
359
|
+
const result = this.reducer.applyPatch(input);
|
|
360
|
+
this.handleAppliedState(handlers, "patch", result, event, messageId, messageOrdinal);
|
|
361
|
+
}
|
|
362
|
+
handleProgress(event, handlers) {
|
|
363
|
+
const payload = event.payload;
|
|
364
|
+
if (!isContentBlockArray(payload.content))
|
|
365
|
+
return;
|
|
366
|
+
const current = this.progressState ??
|
|
367
|
+
this.reducer.get({ spaceId: this.spaceId, sessionId: this.sessionId });
|
|
368
|
+
const turnId = typeof payload.turnId === "string" ? payload.turnId : current.turnId;
|
|
369
|
+
const anchorUserMessageId = typeof payload.anchorUserMessageId === "string"
|
|
370
|
+
? payload.anchorUserMessageId
|
|
371
|
+
: current.anchorUserMessageId;
|
|
372
|
+
const messageOrdinal = typeof payload.messageOrdinal === "number"
|
|
373
|
+
? payload.messageOrdinal
|
|
374
|
+
: this.messageOrdinal;
|
|
375
|
+
const messageId = this.prepareMessageBoundary({
|
|
376
|
+
turnId,
|
|
377
|
+
messageId: typeof payload.messageId === "string" ? payload.messageId : this.messageId,
|
|
378
|
+
messageOrdinal,
|
|
379
|
+
anchorUserMessageId,
|
|
380
|
+
});
|
|
381
|
+
const base = this.progressState?.turnId === turnId ? this.progressState : current;
|
|
382
|
+
const state = {
|
|
383
|
+
...base,
|
|
384
|
+
spaceId: this.spaceId,
|
|
385
|
+
sessionId: this.sessionId,
|
|
386
|
+
status: "streaming",
|
|
387
|
+
contentBlocks: mergeStreamingDeltaBlocks(base.contentBlocks, payload.content),
|
|
388
|
+
anchorUserMessageId,
|
|
389
|
+
turnId,
|
|
390
|
+
};
|
|
391
|
+
this.progressState = state;
|
|
392
|
+
this.messageId = messageId;
|
|
393
|
+
this.messageOrdinal = messageOrdinal;
|
|
394
|
+
this.emit(handlers, {
|
|
395
|
+
type: "state",
|
|
396
|
+
source: "progress",
|
|
397
|
+
state,
|
|
398
|
+
messageId,
|
|
399
|
+
messageOrdinal,
|
|
400
|
+
intermediateMessages: [...this.intermediateMessages],
|
|
401
|
+
rawEvent: event,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
handlePersisted(event, handlers) {
|
|
405
|
+
const message = event.payload.message;
|
|
406
|
+
if (!isRecord(message))
|
|
407
|
+
return;
|
|
408
|
+
const commit = parseAssistantMessageCommit(message);
|
|
409
|
+
if (commit.kind === "intermediate") {
|
|
410
|
+
const intermediate = messageRecordToIntermediate(commit.message);
|
|
411
|
+
if (intermediate) {
|
|
412
|
+
this.addIntermediateMessage(intermediate);
|
|
413
|
+
}
|
|
414
|
+
this.reducer.reset({ spaceId: this.spaceId, sessionId: this.sessionId });
|
|
415
|
+
this.resetCurrentMessage();
|
|
416
|
+
}
|
|
417
|
+
if (commit.kind === "final") {
|
|
418
|
+
this.reducer.complete({
|
|
419
|
+
spaceId: this.spaceId,
|
|
420
|
+
sessionId: this.sessionId,
|
|
421
|
+
turnId: getTurnIdFromMessage(commit.message),
|
|
422
|
+
});
|
|
423
|
+
this.resetCurrentMessage();
|
|
424
|
+
}
|
|
425
|
+
if (commit.kind === "error") {
|
|
426
|
+
this.reducer.fail({
|
|
427
|
+
spaceId: this.spaceId,
|
|
428
|
+
sessionId: this.sessionId,
|
|
429
|
+
turnId: getTurnIdFromMessage(commit.message),
|
|
430
|
+
});
|
|
431
|
+
this.resetCurrentMessage();
|
|
432
|
+
}
|
|
433
|
+
this.emit(handlers, {
|
|
434
|
+
type: "commit",
|
|
435
|
+
commit,
|
|
436
|
+
rawEvent: event,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
handleFinalized(event, handlers) {
|
|
440
|
+
const turn = event.payload.turn;
|
|
441
|
+
if (!isRecord(turn))
|
|
442
|
+
return;
|
|
443
|
+
const typedTurn = turn;
|
|
444
|
+
if (typedTurn.status === "interrupted") {
|
|
445
|
+
this.reducer.interrupt({
|
|
446
|
+
spaceId: this.spaceId,
|
|
447
|
+
sessionId: this.sessionId,
|
|
448
|
+
turnId: typedTurn.id,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
this.reducer.complete({
|
|
453
|
+
spaceId: this.spaceId,
|
|
454
|
+
sessionId: this.sessionId,
|
|
455
|
+
turnId: typedTurn.id,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
this.resetCurrentMessage();
|
|
459
|
+
this.emit(handlers, {
|
|
460
|
+
type: "finalized",
|
|
461
|
+
turn: typedTurn,
|
|
462
|
+
rawEvent: event,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
handleEvent(event, handlers) {
|
|
466
|
+
switch (event.type) {
|
|
467
|
+
case "session.turn.snapshot":
|
|
468
|
+
this.handleSnapshot(event, handlers);
|
|
469
|
+
return;
|
|
470
|
+
case "session.turn.patch":
|
|
471
|
+
this.handlePatch(event, handlers);
|
|
472
|
+
return;
|
|
473
|
+
case "session.turn.progress":
|
|
474
|
+
this.handleProgress(event, handlers);
|
|
475
|
+
return;
|
|
476
|
+
case "session.message.persisted":
|
|
477
|
+
this.handlePersisted(event, handlers);
|
|
478
|
+
return;
|
|
479
|
+
case "session.turn.finalized":
|
|
480
|
+
this.handleFinalized(event, handlers);
|
|
481
|
+
return;
|
|
482
|
+
case "session.turn.updated": {
|
|
483
|
+
const turn = event.payload.turn;
|
|
484
|
+
if (!isRecord(turn))
|
|
485
|
+
return;
|
|
486
|
+
this.emit(handlers, {
|
|
487
|
+
type: "turn_updated",
|
|
488
|
+
turn: turn,
|
|
489
|
+
rawEvent: event,
|
|
490
|
+
});
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
case "session.turn.error": {
|
|
494
|
+
const message = typeof event.payload.error === "string" && event.payload.error.trim()
|
|
495
|
+
? event.payload.error.trim()
|
|
496
|
+
: "Generation failed";
|
|
497
|
+
this.reducer.fail({ spaceId: this.spaceId, sessionId: this.sessionId });
|
|
498
|
+
this.resetCurrentMessage();
|
|
499
|
+
this.emit(handlers, {
|
|
500
|
+
type: "error",
|
|
501
|
+
message,
|
|
502
|
+
rawEvent: event,
|
|
503
|
+
});
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
default:
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
export function createSessionGenerationStreamClient(input) {
|
|
512
|
+
return new SessionGenerationStreamClient(input.websocketClient, input.spaceId, input.sessionId);
|
|
513
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ContentBlock } from "@neta-art/cohub-protocol/core";
|
|
2
2
|
import type { RealtimePatchOperation, SessionTurnPatchEvent } from "@neta-art/cohub-protocol/realtime";
|
|
3
|
-
export type SessionPatchStatus = "idle" | "pending" | "streaming" | "completed" | "failed";
|
|
3
|
+
export type SessionPatchStatus = "idle" | "pending" | "streaming" | "completed" | "failed" | "interrupted";
|
|
4
4
|
export type SessionPatchState = {
|
|
5
5
|
spaceId: string | null;
|
|
6
6
|
sessionId: string;
|
|
@@ -50,6 +50,7 @@ export declare class SessionPatchReducer {
|
|
|
50
50
|
}): SessionPatchState;
|
|
51
51
|
complete(input: SessionPatchKeyInput): SessionPatchState;
|
|
52
52
|
fail(input: SessionPatchKeyInput): SessionPatchState;
|
|
53
|
+
interrupt(input: SessionPatchKeyInput): SessionPatchState;
|
|
53
54
|
reset(input: SessionPatchKeyInput): void;
|
|
54
55
|
applySnapshot(input: SessionPatchSnapshotInput): SessionPatchApplyResult;
|
|
55
56
|
resetAll(): void;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const blockSubPathPattern = /^\/message\/content\/blocks\/(\d+)\/(.+)$/;
|
|
2
2
|
const blockPathPattern = /^\/message\/content\/blocks\/(\d+)$/;
|
|
3
3
|
const blockMetaPathPattern = /^\/message\/content\/blocks\/(\d+)\/_meta$/;
|
|
4
|
+
const TERMINAL_PATCH_STATUSES = new Set(["completed", "failed", "interrupted"]);
|
|
5
|
+
const isTerminalPatchStatus = (status) => TERMINAL_PATCH_STATUSES.has(status);
|
|
4
6
|
const createIdleState = (input) => ({
|
|
5
7
|
spaceId: input.spaceId ?? null,
|
|
6
8
|
sessionId: input.sessionId,
|
|
@@ -18,9 +20,27 @@ function getStreamIndex(block) {
|
|
|
18
20
|
const value = block._meta?.streamIndex;
|
|
19
21
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
20
22
|
}
|
|
23
|
+
function blockIdentityCompatible(prev, next) {
|
|
24
|
+
if (prev.type !== next.type)
|
|
25
|
+
return false;
|
|
26
|
+
if (prev.type === "tool_use" && next.type === "tool_use")
|
|
27
|
+
return prev.id === next.id && prev.name === next.name;
|
|
28
|
+
if (prev.type === "tool_result" && next.type === "tool_result")
|
|
29
|
+
return prev.tool_use_id === next.tool_use_id;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
21
32
|
function findBlockByStreamIndex(blocks, streamIndex) {
|
|
22
33
|
return blocks.findIndex((block) => getStreamIndex(block) === streamIndex);
|
|
23
34
|
}
|
|
35
|
+
function findBlockForReplacement(blocks, streamIndex, nextBlock) {
|
|
36
|
+
const compatibleIndex = blocks.findIndex((block) => getStreamIndex(block) === streamIndex && blockIdentityCompatible(block, nextBlock));
|
|
37
|
+
if (compatibleIndex >= 0)
|
|
38
|
+
return compatibleIndex;
|
|
39
|
+
const sameStreamIndex = blocks.findIndex((block) => getStreamIndex(block) === streamIndex);
|
|
40
|
+
if (sameStreamIndex >= 0 && nextBlock.type !== "tool_use" && nextBlock.type !== "tool_result")
|
|
41
|
+
return sameStreamIndex;
|
|
42
|
+
return -1;
|
|
43
|
+
}
|
|
24
44
|
function sortBlocksByStreamIndex(blocks) {
|
|
25
45
|
return [...blocks].sort((a, b) => {
|
|
26
46
|
const aIndex = getStreamIndex(a);
|
|
@@ -225,7 +245,7 @@ function applyPatchOpsToBlocks(current, ops, initialAppendPath) {
|
|
|
225
245
|
const streamIndex = Number(match[1]);
|
|
226
246
|
const block = cloneBlock(op.v);
|
|
227
247
|
block._meta = { ...(block._meta ?? {}), streamIndex };
|
|
228
|
-
const blockIndex =
|
|
248
|
+
const blockIndex = findBlockForReplacement(next, streamIndex, block);
|
|
229
249
|
if (blockIndex >= 0) {
|
|
230
250
|
next[blockIndex] = block;
|
|
231
251
|
}
|
|
@@ -305,6 +325,19 @@ export class SessionPatchReducer {
|
|
|
305
325
|
this.states.set(this.key(input), state);
|
|
306
326
|
return state;
|
|
307
327
|
}
|
|
328
|
+
interrupt(input) {
|
|
329
|
+
const current = this.get(input);
|
|
330
|
+
const state = {
|
|
331
|
+
...current,
|
|
332
|
+
turnId: input.turnId ?? current.turnId,
|
|
333
|
+
status: "interrupted",
|
|
334
|
+
contentBlocks: [],
|
|
335
|
+
anchorUserMessageId: null,
|
|
336
|
+
appendPath: null,
|
|
337
|
+
};
|
|
338
|
+
this.states.set(this.key(input), state);
|
|
339
|
+
return state;
|
|
340
|
+
}
|
|
308
341
|
reset(input) {
|
|
309
342
|
this.states.delete(this.key(input));
|
|
310
343
|
}
|
|
@@ -313,7 +346,8 @@ export class SessionPatchReducer {
|
|
|
313
346
|
const inputTurnId = input.turnId ?? null;
|
|
314
347
|
const currentTurnId = current.turnId;
|
|
315
348
|
const isDifferentKnownTurn = Boolean(currentTurnId && inputTurnId && currentTurnId !== inputTurnId);
|
|
316
|
-
|
|
349
|
+
const isTerminalSameTurn = isTerminalPatchStatus(current.status) && Boolean(currentTurnId) && currentTurnId === inputTurnId;
|
|
350
|
+
if (isTerminalSameTurn || (!isDifferentKnownTurn && input.seq < current.patchSeq)) {
|
|
317
351
|
return { applied: false, reason: "duplicate", state: current };
|
|
318
352
|
}
|
|
319
353
|
const state = {
|
|
@@ -356,7 +390,7 @@ export class SessionPatchReducer {
|
|
|
356
390
|
currentTurnId === inputTurnId &&
|
|
357
391
|
input.baseSeq === 0 &&
|
|
358
392
|
input.seq >= currentSeq);
|
|
359
|
-
const isTerminalSameTurn = (current.status
|
|
393
|
+
const isTerminalSameTurn = isTerminalPatchStatus(current.status) &&
|
|
360
394
|
Boolean(currentTurnId) &&
|
|
361
395
|
currentTurnId === inputTurnId;
|
|
362
396
|
if (isTerminalSameTurn) {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { SessionBindingRecord as ProtocolSessionBindingRecord, SessionRecord as ProtocolSessionRecord, SessionTurnIndexItem, SessionTurnRecord } from "@neta-art/cohub-protocol/model";
|
|
1
|
+
import type { SessionBindingRecord as ProtocolSessionBindingRecord, SessionRecord as ProtocolSessionRecord, SessionForkRecord, SessionTurnIndexItem, SessionTurnRecord, SessionTurnSegmentRecord } from "@neta-art/cohub-protocol/model";
|
|
2
2
|
import type { ChannelConfig } from "@neta-art/cohub-protocol/gateway";
|
|
3
|
-
import type { ContentBlock } from "@neta-art/cohub-protocol/core";
|
|
3
|
+
import type { ContentBlock, Usage } from "@neta-art/cohub-protocol/core";
|
|
4
4
|
import type { MessageRecord } from "@neta-art/cohub-protocol/model";
|
|
5
5
|
export type { ChannelConfig, DiscordChannelConfig, } from "@neta-art/cohub-protocol/gateway";
|
|
6
6
|
export type ApiError = {
|
|
@@ -24,7 +24,7 @@ export type UserRulesResponse = {
|
|
|
24
24
|
source: "config-space";
|
|
25
25
|
path: string;
|
|
26
26
|
};
|
|
27
|
-
export type { ContentBlock, MessageRecord, SessionTurnRecord, SessionTurnIndexItem };
|
|
27
|
+
export type { ContentBlock, MessageRecord, SessionTurnRecord, SessionTurnIndexItem, SessionForkRecord, SessionTurnSegmentRecord };
|
|
28
28
|
export type SpaceFsEntry = {
|
|
29
29
|
name: string;
|
|
30
30
|
path: string;
|
|
@@ -226,6 +226,18 @@ export type SessionTurnStreamSnapshotResponse = {
|
|
|
226
226
|
messageId: string | null;
|
|
227
227
|
messageOrdinal: number | null;
|
|
228
228
|
content: ContentBlock[];
|
|
229
|
+
id?: string;
|
|
230
|
+
sessionId?: string;
|
|
231
|
+
role?: "user" | "assistant" | "system";
|
|
232
|
+
text?: string | null;
|
|
233
|
+
provider?: string | null;
|
|
234
|
+
model?: string | null;
|
|
235
|
+
stopReason?: string | null;
|
|
236
|
+
errorMessage?: string | null;
|
|
237
|
+
usage?: Usage | null;
|
|
238
|
+
toolCallsObjectKey?: string | null;
|
|
239
|
+
meta?: Record<string, unknown> | null;
|
|
240
|
+
createdAt?: string;
|
|
229
241
|
}>;
|
|
230
242
|
updatedAt: number;
|
|
231
243
|
} | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Cohub SDK for spaces, sessions, checkpoints, and realtime agent collaboration.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"README.md"
|
|
45
45
|
],
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@neta-art/cohub-protocol": "1.
|
|
47
|
+
"@neta-art/cohub-protocol": "1.5.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"typescript": "^6.0.3"
|