@prur/dsh-chat-service 0.1.15 → 0.1.16
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/lib/engine.d.ts +47 -6
- package/lib/engine.js +50 -6
- package/lib/index.d.ts +46 -9
- package/lib/index.js +330 -51
- package/lib/runner.d.ts +13 -2
- package/lib/runner.js +27 -14
- package/lib/store.d.ts +57 -2
- package/lib/store.js +140 -2
- package/lib/typert.host.js +67 -3
- package/lib/typert.remote-client.js +23 -0
- package/lib/types.d.ts +80 -2
- package/package.json +3 -2
package/lib/engine.d.ts
CHANGED
|
@@ -4,20 +4,48 @@
|
|
|
4
4
|
* in this repo directly against these functions.
|
|
5
5
|
* @module @prur/dsh-chat-service/src/engine
|
|
6
6
|
*/
|
|
7
|
-
import type { ChatMessage } from './types.ts';
|
|
7
|
+
import type { ChatImageAttachmentRef, ChatMessage } from './types.ts';
|
|
8
8
|
/** Truncated title derived from the first user message. */
|
|
9
9
|
export declare const TITLE_MAX_CHARS = 30;
|
|
10
10
|
/** Rough token estimate (4 chars per token) for context-window trimming. */
|
|
11
11
|
export declare const CHARS_PER_TOKEN = 4;
|
|
12
|
+
/**
|
|
13
|
+
* Rough token estimate for one image block: one token per 85×85 tile
|
|
14
|
+
* (the conventional vision-model pricing granularity), rounding up.
|
|
15
|
+
*/
|
|
16
|
+
export declare const IMAGE_TILE_EDGE = 85;
|
|
12
17
|
/** Context-window utilization kept under this fraction before trimming. */
|
|
13
18
|
export declare const CONTEXT_WINDOW_SAFETY = 0.9;
|
|
19
|
+
/** One model-facing content part produced by context assembly. */
|
|
20
|
+
export type AssembledContentPart = {
|
|
21
|
+
readonly type: 'text';
|
|
22
|
+
readonly text: string;
|
|
23
|
+
} | {
|
|
24
|
+
readonly type: 'image';
|
|
25
|
+
readonly attachment: ChatImageAttachmentRef;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The producer provenance DSH's `ctx.llm.stream` requires on every message
|
|
29
|
+
* (`Message.source`, `message.ts`). Without it the llm layer's `forAdapter`
|
|
30
|
+
* reads `source.kind` on an assistant message and throws
|
|
31
|
+
* "Cannot read properties of undefined (reading 'kind')" — the chat multi-turn
|
|
32
|
+
* failure. A user message is `{kind:'user'}`; an assistant message is
|
|
33
|
+
* `{kind:'model', provider, model}` (no `replayState`, so the llm layer returns
|
|
34
|
+
* the message unchanged rather than rewriting its provenance).
|
|
35
|
+
*/
|
|
36
|
+
export type AssembledMessageSource = {
|
|
37
|
+
readonly kind: 'user';
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: 'model';
|
|
40
|
+
readonly provider: string;
|
|
41
|
+
readonly model: string;
|
|
42
|
+
};
|
|
14
43
|
/** One model-facing message produced by context assembly. */
|
|
15
44
|
export interface AssembledMessage {
|
|
16
45
|
readonly role: 'user' | 'assistant';
|
|
17
|
-
readonly content: readonly
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}[];
|
|
46
|
+
readonly content: readonly AssembledContentPart[];
|
|
47
|
+
/** Required by DSH's llm layer; see {@link AssembledMessageSource}. */
|
|
48
|
+
readonly source: AssembledMessageSource;
|
|
21
49
|
}
|
|
22
50
|
/** Derived one-line title from a prompt's text; null for empty prompts. */
|
|
23
51
|
export declare function deriveTitle(text: string): string | null;
|
|
@@ -28,6 +56,19 @@ export interface ContextAssemblyResult {
|
|
|
28
56
|
/** True when the context-window estimate trimmed older messages. */
|
|
29
57
|
readonly windowTrimmed: boolean;
|
|
30
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the regenerate pair: the last assistant message and THE user
|
|
61
|
+
* message it answers (turn association). Returns undefined when there is no
|
|
62
|
+
* assistant message at all; `user` is undefined when the turn association is
|
|
63
|
+
* broken — the user message immediately preceding the assistant is missing
|
|
64
|
+
* (deleted) or not the last row before it (an intervening message makes the
|
|
65
|
+
* association ambiguous), which the service rejects rather than silently
|
|
66
|
+
* regenerating an unrelated older prompt (review P1).
|
|
67
|
+
*/
|
|
68
|
+
export declare function regenerateTarget(history: readonly ChatMessage[]): {
|
|
69
|
+
assistant: ChatMessage;
|
|
70
|
+
user: ChatMessage | undefined;
|
|
71
|
+
} | undefined;
|
|
31
72
|
/**
|
|
32
73
|
* Assemble the model-facing message list for one turn. System marker rows and
|
|
33
74
|
* failed assistant messages are excluded; `contextMessages` (when set) keeps
|
|
@@ -35,5 +76,5 @@ export interface ContextAssemblyResult {
|
|
|
35
76
|
* messages when the rough token count would overflow the model window.
|
|
36
77
|
*/
|
|
37
78
|
export declare function assembleContext(history: readonly ChatMessage[], contextMessages: number | null, contextWindow: number | null): ContextAssemblyResult;
|
|
38
|
-
/** Rough token estimate for one message's text blocks. */
|
|
79
|
+
/** Rough token estimate for one message's text and image blocks. */
|
|
39
80
|
export declare function estimateTokens(message: ChatMessage): number;
|
package/lib/engine.js
CHANGED
|
@@ -8,8 +8,19 @@
|
|
|
8
8
|
export const TITLE_MAX_CHARS = 30;
|
|
9
9
|
/** Rough token estimate (4 chars per token) for context-window trimming. */
|
|
10
10
|
export const CHARS_PER_TOKEN = 4;
|
|
11
|
+
/**
|
|
12
|
+
* Rough token estimate for one image block: one token per 85×85 tile
|
|
13
|
+
* (the conventional vision-model pricing granularity), rounding up.
|
|
14
|
+
*/
|
|
15
|
+
export const IMAGE_TILE_EDGE = 85;
|
|
11
16
|
/** Context-window utilization kept under this fraction before trimming. */
|
|
12
17
|
export const CONTEXT_WINDOW_SAFETY = 0.9;
|
|
18
|
+
/** Provenance for an assembled message derived from its chat log row. */
|
|
19
|
+
function sourceFor(role, message) {
|
|
20
|
+
return role === 'user'
|
|
21
|
+
? { kind: 'user' }
|
|
22
|
+
: { kind: 'model', provider: message.provider ?? 'unknown', model: message.model ?? 'unknown' };
|
|
23
|
+
}
|
|
13
24
|
/** Derived one-line title from a prompt's text; null for empty prompts. */
|
|
14
25
|
export function deriveTitle(text) {
|
|
15
26
|
const firstLine = text.replace(/\s+/g, ' ').trim();
|
|
@@ -22,6 +33,28 @@ export function deriveTitle(text) {
|
|
|
22
33
|
function isUsable(message) {
|
|
23
34
|
return message.role !== 'system' && message.error === undefined;
|
|
24
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the regenerate pair: the last assistant message and THE user
|
|
38
|
+
* message it answers (turn association). Returns undefined when there is no
|
|
39
|
+
* assistant message at all; `user` is undefined when the turn association is
|
|
40
|
+
* broken — the user message immediately preceding the assistant is missing
|
|
41
|
+
* (deleted) or not the last row before it (an intervening message makes the
|
|
42
|
+
* association ambiguous), which the service rejects rather than silently
|
|
43
|
+
* regenerating an unrelated older prompt (review P1).
|
|
44
|
+
*/
|
|
45
|
+
export function regenerateTarget(history) {
|
|
46
|
+
for (let index = history.length - 1; index >= 0; index -= 1) {
|
|
47
|
+
const candidate = history[index];
|
|
48
|
+
if (candidate.role !== 'assistant')
|
|
49
|
+
continue;
|
|
50
|
+
const previous = history[index - 1];
|
|
51
|
+
if (previous !== undefined && previous.role === 'user') {
|
|
52
|
+
return { assistant: candidate, user: previous };
|
|
53
|
+
}
|
|
54
|
+
return { assistant: candidate, user: undefined };
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
25
58
|
/**
|
|
26
59
|
* Assemble the model-facing message list for one turn. System marker rows and
|
|
27
60
|
* failed assistant messages are excluded; `contextMessages` (when set) keeps
|
|
@@ -56,16 +89,27 @@ export function assembleContext(history, contextMessages, contextWindow) {
|
|
|
56
89
|
return {
|
|
57
90
|
messages: selected.map((message) => ({
|
|
58
91
|
role: message.role,
|
|
59
|
-
content: message.blocks.map(block =>
|
|
92
|
+
content: message.blocks.map(block => block.type === 'image'
|
|
93
|
+
? { type: 'image', attachment: block.attachment }
|
|
94
|
+
: { type: 'text', text: block.text }),
|
|
95
|
+
source: sourceFor(message.role, message),
|
|
60
96
|
})),
|
|
61
97
|
contextMessages,
|
|
62
98
|
windowTrimmed,
|
|
63
99
|
};
|
|
64
100
|
}
|
|
65
|
-
/** Rough token estimate for one message's text blocks. */
|
|
101
|
+
/** Rough token estimate for one message's text and image blocks. */
|
|
66
102
|
export function estimateTokens(message) {
|
|
67
|
-
let
|
|
68
|
-
for (const block of message.blocks)
|
|
69
|
-
|
|
70
|
-
|
|
103
|
+
let tokens = 0;
|
|
104
|
+
for (const block of message.blocks) {
|
|
105
|
+
if (block.type === 'image') {
|
|
106
|
+
const widthTiles = Math.ceil(block.attachment.width / IMAGE_TILE_EDGE);
|
|
107
|
+
const heightTiles = Math.ceil(block.attachment.height / IMAGE_TILE_EDGE);
|
|
108
|
+
tokens += Math.max(1, widthTiles * heightTiles);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
tokens += Math.ceil(block.text.length / CHARS_PER_TOKEN);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return tokens;
|
|
71
115
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { Context } from '@deepseek-ai/cordis';
|
|
|
11
11
|
import z from '@deepseek-ai/schemastery';
|
|
12
12
|
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
13
13
|
import { ChatStorageError } from './store.ts';
|
|
14
|
-
import type { ChatCreateResult, ChatEmptyResult, ChatHistoryResult, ChatListResult, ChatSelectModelResult, ChatSendResult,
|
|
14
|
+
import type { ChatAttachmentResult, ChatCapabilitiesResult, ChatContentPart, ChatCreateResult, ChatDeleteMessageResult, ChatEmptyResult, ChatHistoryResult, ChatListResult, ChatRegenerateResult, ChatSelectModelResult, ChatSendResult, ChatUpdatePatch, ChatUpdateResult } from './types.ts';
|
|
15
15
|
/** Stable Cordis plugin name. */
|
|
16
16
|
export declare const name = "chat-service";
|
|
17
17
|
declare module '@deepseek-ai/cordis' {
|
|
@@ -28,8 +28,8 @@ declare module '@deepseek-ai/cordis' {
|
|
|
28
28
|
}
|
|
29
29
|
/** Business failure carrying a stable code for diagnostics. */
|
|
30
30
|
export declare class ChatServiceError extends Error {
|
|
31
|
-
readonly code: 'no-model' | 'invalid-request' | 'unsupported-block';
|
|
32
|
-
constructor(code: 'no-model' | 'invalid-request' | 'unsupported-block', message: string);
|
|
31
|
+
readonly code: 'no-model' | 'invalid-request' | 'unsupported-block' | 'image-unsupported';
|
|
32
|
+
constructor(code: 'no-model' | 'invalid-request' | 'unsupported-block' | 'image-unsupported', message: string);
|
|
33
33
|
}
|
|
34
34
|
/** Resolve the harness home the chat store lives under (env overrides default). */
|
|
35
35
|
export declare function chatHomePath(): string;
|
|
@@ -70,19 +70,56 @@ export declare class ChatService extends TypertRemoteService {
|
|
|
70
70
|
/**
|
|
71
71
|
* Send one user turn into a conversation: the user message persists and is
|
|
72
72
|
* anchored through `chat/message`, then its turn joins the FIFO queue.
|
|
73
|
+
* Image parts are admitted against the deployment attachment limits and
|
|
74
|
+
* promoted to durable references on the persisted message (design §3).
|
|
73
75
|
*/
|
|
74
|
-
send(conversationId: string, content: readonly
|
|
76
|
+
send(conversationId: string, content: readonly ChatContentPart[]): Promise<ChatSendResult>;
|
|
77
|
+
/**
|
|
78
|
+
* Toggle a conversation's archive flag (M4). Archived conversations keep
|
|
79
|
+
* their messages and settings; the client hides them from the active view.
|
|
80
|
+
*/
|
|
81
|
+
archive(conversationId: string, archived: boolean): Promise<ChatEmptyResult>;
|
|
82
|
+
/**
|
|
83
|
+
* Delete a single message (design §2.3): the host rewrites the message
|
|
84
|
+
* file, so subsequent context assembly excludes it. Queued sends bound to
|
|
85
|
+
* the deleted seq are dropped too; an in-flight turn keeps its assembled
|
|
86
|
+
* context and settles normally. Idempotent: absent seq reports removed:
|
|
87
|
+
* false without error.
|
|
88
|
+
*/
|
|
89
|
+
deleteMessage(conversationId: string, seq: number): Promise<ChatDeleteMessageResult>;
|
|
90
|
+
/**
|
|
91
|
+
* Regenerate the last assistant message (design §3): remove it, then rerun
|
|
92
|
+
* the last user turn with the same context boundary. Rejected while a turn
|
|
93
|
+
* is running or when there is no assistant message to regenerate.
|
|
94
|
+
*/
|
|
95
|
+
regenerate(conversationId: string): Promise<ChatRegenerateResult>;
|
|
96
|
+
/**
|
|
97
|
+
* Model capability probe for the conversation's current selection
|
|
98
|
+
* (adapter metadata; design §4.6): the client preflights image sending
|
|
99
|
+
* instead of discovering a refusal after the bytes were picked.
|
|
100
|
+
*/
|
|
101
|
+
capabilities(conversationId: string): Promise<ChatCapabilitiesResult>;
|
|
102
|
+
/**
|
|
103
|
+
* Read one image's bytes after proving the conversation's message log
|
|
104
|
+
* references its attachment id (mirror of `session.attachment`'s
|
|
105
|
+
* authorization discipline; design §3 读图).
|
|
106
|
+
*/
|
|
107
|
+
attachment(conversationId: string, attachmentId: string): Promise<ChatAttachmentResult>;
|
|
75
108
|
/** Abort the in-flight turn (frozen partial is persisted); idempotent. */
|
|
76
109
|
cancel(conversationId: string): Promise<ChatEmptyResult>;
|
|
77
110
|
/** Switch the conversation's model selection. */
|
|
78
111
|
selectModel(conversationId: string, provider: string, model: string): Promise<ChatSelectModelResult>;
|
|
79
112
|
/** Update conversation settings; `null` in the patch unsets a field. */
|
|
80
|
-
update(conversationId: string, patch:
|
|
81
|
-
readonly systemPrompt?: string | null;
|
|
82
|
-
readonly temperature?: number | null;
|
|
83
|
-
readonly contextMessages?: number | null;
|
|
84
|
-
}): Promise<ChatUpdateResult>;
|
|
113
|
+
update(conversationId: string, patch: ChatUpdatePatch): Promise<ChatUpdateResult>;
|
|
85
114
|
private defaultSelection;
|
|
115
|
+
/** Adapter metadata for the conversation's model (structural llm seam). */
|
|
116
|
+
private resolveModelInfo;
|
|
117
|
+
/**
|
|
118
|
+
* Whether the model accepts image input. Unknown capability is a refusal
|
|
119
|
+
* (fail-closed, mirror of the session image admission preflight outcome:
|
|
120
|
+
* an unresolvable adapter must not silently accept an image request).
|
|
121
|
+
*/
|
|
122
|
+
private imageInputAllowed;
|
|
86
123
|
private runnerFor;
|
|
87
124
|
private turnRecord;
|
|
88
125
|
/** Persist turn-start bookkeeping: shift the queued send and mark running. */
|