@celestea/core 2.7.1
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/LICENSE +21 -0
- package/README.md +95 -0
- package/contracts/data-files/checkpoint.schema.json +111 -0
- package/contracts/data-files/cli-main-jsonl-precompact.schema.json +27 -0
- package/contracts/data-files/cli-main-jsonl.schema.json +22 -0
- package/contracts/data-files/fallbacks.schema.json +71 -0
- package/contracts/data-files/index.json +124 -0
- package/contracts/data-files/pricing.schema.json +65 -0
- package/contracts/data-files/prompts.schema.json +130 -0
- package/contracts/data-files/providers.schema.json +177 -0
- package/contracts/data-files/registry-tsv.schema.json +74 -0
- package/contracts/data-files/session.schema.json +51 -0
- package/contracts/data-files/usage-ledger.schema.json +112 -0
- package/contracts/data-files/workspaces.schema.json +63 -0
- package/contracts/endpoints.json +4390 -0
- package/contracts/probe-evidence.json +219 -0
- package/contracts/route-table.snapshot.json +377 -0
- package/contracts/scope-hash-vectors.json +273 -0
- package/contracts/session-event.schema.json +441 -0
- package/contracts/sse-events.json +202 -0
- package/contracts/tools.json +730 -0
- package/dist/agent.d.ts +65 -0
- package/dist/agent.js +36 -0
- package/dist/celestea-home.d.ts +63 -0
- package/dist/celestea-home.js +96 -0
- package/dist/celestea-sources.d.ts +53 -0
- package/dist/celestea-sources.js +61 -0
- package/dist/context.d.ts +33 -0
- package/dist/context.js +55 -0
- package/dist/contracts/index.d.ts +234 -0
- package/dist/contracts/index.js +159 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +22 -0
- package/dist/event-bus.d.ts +60 -0
- package/dist/event-bus.js +100 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +66 -0
- package/dist/injection.d.ts +61 -0
- package/dist/injection.js +27 -0
- package/dist/json.d.ts +34 -0
- package/dist/json.js +127 -0
- package/dist/llm.d.ts +34 -0
- package/dist/llm.js +41 -0
- package/dist/memory.d.ts +72 -0
- package/dist/memory.js +123 -0
- package/dist/message.d.ts +189 -0
- package/dist/message.js +252 -0
- package/dist/plugin.d.ts +38 -0
- package/dist/plugin.js +49 -0
- package/dist/projection.d.ts +67 -0
- package/dist/projection.js +168 -0
- package/dist/question.d.ts +154 -0
- package/dist/question.js +82 -0
- package/dist/redact.d.ts +40 -0
- package/dist/redact.js +185 -0
- package/dist/repo.d.ts +14 -0
- package/dist/repo.js +87 -0
- package/dist/sandbox.d.ts +182 -0
- package/dist/sandbox.js +78 -0
- package/dist/session-event.d.ts +57 -0
- package/dist/session-event.js +425 -0
- package/dist/session-log.d.ts +71 -0
- package/dist/session-log.js +66 -0
- package/dist/skill-catalog.d.ts +29 -0
- package/dist/skill-catalog.js +52 -0
- package/dist/skills.d.ts +116 -0
- package/dist/skills.js +273 -0
- package/dist/sse-bus.d.ts +40 -0
- package/dist/sse-bus.js +105 -0
- package/dist/stream.d.ts +115 -0
- package/dist/stream.js +52 -0
- package/dist/tool-surface.d.ts +45 -0
- package/dist/tool-surface.js +98 -0
- package/dist/tool.d.ts +77 -0
- package/dist/tool.js +15 -0
- package/dist/turn-id.d.ts +37 -0
- package/dist/turn-id.js +76 -0
- package/dist/types.d.ts +396 -0
- package/dist/types.js +58 -0
- package/package.json +27 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model-visible message model — a 1:1 port of
|
|
3
|
+
* `celestea_harness/crates/core/src/message.rs`.
|
|
4
|
+
*
|
|
5
|
+
* Shapes ported here:
|
|
6
|
+
* Role enum { System, User, Assistant, Tool } (serde lowercase)
|
|
7
|
+
* ToolCall { id, name, args: Value }
|
|
8
|
+
* Content enum { Text(String), ToolCall(ToolCall) } (tag="type", content="content")
|
|
9
|
+
* Message { role, content: Vec<Content>, tool_call_id: Option<String> }
|
|
10
|
+
* ToolSpec { name, description, parameters } (declared in ./types.ts)
|
|
11
|
+
* Usage { prompt_tokens, completion_tokens, total_tokens,
|
|
12
|
+
* cache_read, reasoning_tokens } (flat counters)
|
|
13
|
+
*
|
|
14
|
+
* Field names and content-tag names are contract, not style: the LLM request
|
|
15
|
+
* builder and every engine projection depend on them. Do not rename anything.
|
|
16
|
+
*/
|
|
17
|
+
/** `Role` — serde `rename_all = "lowercase"`. */
|
|
18
|
+
export declare const ROLES: readonly ["system", "user", "assistant", "tool"];
|
|
19
|
+
export type Role = (typeof ROLES)[number];
|
|
20
|
+
/** `ToolCall` — the id is the provider call id, `args` is the raw JSON value. */
|
|
21
|
+
export interface ToolCall {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
args: unknown;
|
|
25
|
+
}
|
|
26
|
+
/** `Content::Text` — `{"type":"text","content":"…"}`. */
|
|
27
|
+
export interface TextContent {
|
|
28
|
+
type: "text";
|
|
29
|
+
content: string;
|
|
30
|
+
}
|
|
31
|
+
/** `Content::ToolCall` — `{"type":"tool_call","content":{…}}`. */
|
|
32
|
+
export interface ToolCallContent {
|
|
33
|
+
type: "tool_call";
|
|
34
|
+
content: ToolCall;
|
|
35
|
+
}
|
|
36
|
+
/** The four media types the attachment pipeline accepts (section 5.4 magic-byte whitelist). */
|
|
37
|
+
export declare const IMAGE_MEDIA_TYPES: readonly ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
38
|
+
export type ImageMediaType = (typeof IMAGE_MEDIA_TYPES)[number];
|
|
39
|
+
/** True for one of the four accepted normalized media types. */
|
|
40
|
+
export declare function isImageMediaType(v: unknown): v is ImageMediaType;
|
|
41
|
+
/**
|
|
42
|
+
* W804 (multimodal P0 4.1): a content-addressed REFERENCE to an attachment
|
|
43
|
+
* object, never the bytes. The object itself lives at
|
|
44
|
+
* <session-dir>/attachments/<attachment_id>.<ext> (section 5); the session log
|
|
45
|
+
* and every projected event carry only this shape — base64 in the log is a red line.
|
|
46
|
+
*
|
|
47
|
+
* original records the pre-normalization dimensions of an image that was
|
|
48
|
+
* re-encoded/downscaled (DSH originalDimensions semantics); P0 never writes it
|
|
49
|
+
* (no re-encode), but the type is frozen here so P1 is purely additive.
|
|
50
|
+
*/
|
|
51
|
+
export interface ImageRef {
|
|
52
|
+
/** Content-addressing id: sha256(original bytes) in lowercase hex. */
|
|
53
|
+
attachment_id: string;
|
|
54
|
+
/** Normalized media type (magic-byte sniffed, never trusted from a name). */
|
|
55
|
+
media_type: ImageMediaType;
|
|
56
|
+
/** Normalized pixel width. */
|
|
57
|
+
width: number;
|
|
58
|
+
/** Normalized pixel height. */
|
|
59
|
+
height: number;
|
|
60
|
+
/** Original upload/file name, for the UI and model-readable labels only. */
|
|
61
|
+
name?: string;
|
|
62
|
+
/** Set when the stored bytes are a downscaled/re-encoded variant. */
|
|
63
|
+
original?: {
|
|
64
|
+
width: number;
|
|
65
|
+
height: number;
|
|
66
|
+
bytes: number;
|
|
67
|
+
media_type: string;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* W804: the log/event spelling of ImageRef (section 4.3). Structurally identical —
|
|
72
|
+
* the alias exists so the event codec and the content model can each use the
|
|
73
|
+
* vocabulary of their own layer without drifting apart.
|
|
74
|
+
*/
|
|
75
|
+
export type AttachmentRef = ImageRef;
|
|
76
|
+
/** Content::Image — {"type":"image","content":{...ImageRef}} (section 4.1). */
|
|
77
|
+
export interface ImageContent {
|
|
78
|
+
type: "image";
|
|
79
|
+
content: ImageRef;
|
|
80
|
+
}
|
|
81
|
+
export type Content = TextContent | ToolCallContent | ImageContent;
|
|
82
|
+
/**
|
|
83
|
+
* `Message` — one entry of the model-visible history.
|
|
84
|
+
*
|
|
85
|
+
* `content` is always a list (an assistant turn that calls tools carries one
|
|
86
|
+
* `tool_call` item per call in a SINGLE message); `tool_call_id` is set only for
|
|
87
|
+
* `role = "tool"`, so a result can be matched to its call.
|
|
88
|
+
*/
|
|
89
|
+
export interface Message {
|
|
90
|
+
role: Role;
|
|
91
|
+
content: Content[];
|
|
92
|
+
tool_call_id: string | null;
|
|
93
|
+
}
|
|
94
|
+
/** `Message::user`. */
|
|
95
|
+
export declare function userMessage(text: string): Message;
|
|
96
|
+
/** `Message::system`. */
|
|
97
|
+
export declare function systemMessage(text: string): Message;
|
|
98
|
+
/** `Message::assistant_text`. */
|
|
99
|
+
export declare function assistantText(text: string): Message;
|
|
100
|
+
/** `Message::assistant_tool_call`. */
|
|
101
|
+
export declare function assistantToolCall(call: ToolCall): Message;
|
|
102
|
+
/** `Message::tool_result`. */
|
|
103
|
+
export declare function toolResultMessage(id: string, text: string): Message;
|
|
104
|
+
/**
|
|
105
|
+
* W804: a user message that carries text plus image references. The text block
|
|
106
|
+
* stays FIRST and the images follow (section 3.5 ordering rule); no-image callers
|
|
107
|
+
* keep using userMessage so the wire shape is byte-identical when there is
|
|
108
|
+
* nothing to attach.
|
|
109
|
+
*/
|
|
110
|
+
export declare function userMessageWithImages(text: string, images: readonly ImageRef[]): Message;
|
|
111
|
+
/**
|
|
112
|
+
* W804: a tool result that carries the canonical JSON text plus image
|
|
113
|
+
* references (the read_image case, section 6.3). The TEXT remains the value's
|
|
114
|
+
* serde JSON — the model must still see path/dimensions — and the images are
|
|
115
|
+
* separate content blocks.
|
|
116
|
+
*/
|
|
117
|
+
export declare function toolResultWithImages(id: string, text: string, images: readonly ImageRef[]): Message;
|
|
118
|
+
/** Static namespace facade: `Message::user(…)` → `Message.user(…)`. */
|
|
119
|
+
export declare const Message: {
|
|
120
|
+
readonly user: typeof userMessage;
|
|
121
|
+
readonly system: typeof systemMessage;
|
|
122
|
+
readonly assistantText: typeof assistantText;
|
|
123
|
+
readonly assistantToolCall: typeof assistantToolCall;
|
|
124
|
+
readonly toolResult: typeof toolResultMessage;
|
|
125
|
+
readonly userWithImages: typeof userMessageWithImages;
|
|
126
|
+
readonly toolResultWithImages: typeof toolResultWithImages;
|
|
127
|
+
};
|
|
128
|
+
export declare function isTextContent(c: Content): c is TextContent;
|
|
129
|
+
export declare function isToolCallContent(c: Content): c is ToolCallContent;
|
|
130
|
+
export declare function isImageContent(c: Content): c is ImageContent;
|
|
131
|
+
/** Wrap one already-content-addressed reference as a model-visible image block. */
|
|
132
|
+
export declare function imageContent(ref: ImageRef): ImageContent;
|
|
133
|
+
/** The image blocks of a message, in order (references only, never bytes). */
|
|
134
|
+
export declare function messageImages(m: Message): ImageRef[];
|
|
135
|
+
/** The tool calls carried by a message (empty for a text-only message). */
|
|
136
|
+
export declare function messageToolCalls(m: Message): ToolCall[];
|
|
137
|
+
/** The ids of the tool calls carried by a message. */
|
|
138
|
+
export declare function toolCallIds(m: Message): string[];
|
|
139
|
+
/**
|
|
140
|
+
* The text blocks of a message, in order. W804: image blocks are deliberately
|
|
141
|
+
* NOT text — they are never concatenated here (and never stringified into a
|
|
142
|
+
* prompt); a consumer that needs the images reads messageImages.
|
|
143
|
+
*/
|
|
144
|
+
export declare function messageTexts(m: Message): string[];
|
|
145
|
+
/** The single text block of a text-only message, or null. */
|
|
146
|
+
export declare function messageText(m: Message): string | null;
|
|
147
|
+
/** True when the message carries at least one tool call. */
|
|
148
|
+
export declare function hasToolCalls(m: Message): boolean;
|
|
149
|
+
/**
|
|
150
|
+
* True when v has the ImageRef shape the event codec and projection accept.
|
|
151
|
+
* name and original are optional and their absence is the normal case.
|
|
152
|
+
*
|
|
153
|
+
* W834 F07 (R3 batch A): the checks below mirror the FROZEN AttachmentRef
|
|
154
|
+
* schema exactly — a 64-lowercase-hex `attachment_id`, and `width`/`height`
|
|
155
|
+
* that are integers >= 1. The old "string + finite number" test let a
|
|
156
|
+
* non-hex id or a zero/negative/fractional dimension through the codec, into
|
|
157
|
+
* the projection and the model-visible history, while the schema rejected it.
|
|
158
|
+
*/
|
|
159
|
+
export declare function isImageRef(v: unknown): v is ImageRef;
|
|
160
|
+
/**
|
|
161
|
+
* Field-whitelist normalization of one decoded attachment reference: unknown
|
|
162
|
+
* fields are dropped and absent optionals stay omitted (serde style). Returns
|
|
163
|
+
* null when the value is not an ImageRef at all.
|
|
164
|
+
*/
|
|
165
|
+
export declare function normalizeImageRef(v: unknown): ImageRef | null;
|
|
166
|
+
/**
|
|
167
|
+
* The image references carried by a tool_result value (its attachments field,
|
|
168
|
+
* section 6.3). Anything that is not a valid reference is ignored, so a
|
|
169
|
+
* malformed tool value can never smuggle a bogus image into the prompt.
|
|
170
|
+
*/
|
|
171
|
+
export declare function attachmentRefsOfValue(value: unknown): ImageRef[];
|
|
172
|
+
/** Provider-reported token usage for one LLM response (`Usage`). */
|
|
173
|
+
export interface Usage {
|
|
174
|
+
prompt_tokens: number;
|
|
175
|
+
completion_tokens: number;
|
|
176
|
+
total_tokens: number;
|
|
177
|
+
cache_read: number;
|
|
178
|
+
reasoning_tokens: number;
|
|
179
|
+
}
|
|
180
|
+
/** All-zero usage (`Usage::default`). */
|
|
181
|
+
export declare function zeroUsage(): Usage;
|
|
182
|
+
/** `Usage::is_empty` — true when every counter is zero. */
|
|
183
|
+
export declare function usageIsEmpty(u: Usage): boolean;
|
|
184
|
+
/** `Usage::add` — per-field sum, returning a new value. */
|
|
185
|
+
export declare function usageAdd(a: Usage, b: Usage): Usage;
|
|
186
|
+
/** Sum a list of usages (`Usage +=` in a loop). */
|
|
187
|
+
export declare function usageSum(usages: readonly Usage[]): Usage;
|
|
188
|
+
/** Cache-hit ratio as reported by the statusline (`cache_read / prompt_tokens`). */
|
|
189
|
+
export declare function cacheHitRatio(u: Usage): number;
|
package/dist/message.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model-visible message model — a 1:1 port of
|
|
3
|
+
* `celestea_harness/crates/core/src/message.rs`.
|
|
4
|
+
*
|
|
5
|
+
* Shapes ported here:
|
|
6
|
+
* Role enum { System, User, Assistant, Tool } (serde lowercase)
|
|
7
|
+
* ToolCall { id, name, args: Value }
|
|
8
|
+
* Content enum { Text(String), ToolCall(ToolCall) } (tag="type", content="content")
|
|
9
|
+
* Message { role, content: Vec<Content>, tool_call_id: Option<String> }
|
|
10
|
+
* ToolSpec { name, description, parameters } (declared in ./types.ts)
|
|
11
|
+
* Usage { prompt_tokens, completion_tokens, total_tokens,
|
|
12
|
+
* cache_read, reasoning_tokens } (flat counters)
|
|
13
|
+
*
|
|
14
|
+
* Field names and content-tag names are contract, not style: the LLM request
|
|
15
|
+
* builder and every engine projection depend on them. Do not rename anything.
|
|
16
|
+
*/
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Role / Content / ToolCall / Message
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/** `Role` — serde `rename_all = "lowercase"`. */
|
|
21
|
+
export const ROLES = ["system", "user", "assistant", "tool"];
|
|
22
|
+
/** The four media types the attachment pipeline accepts (section 5.4 magic-byte whitelist). */
|
|
23
|
+
export const IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
24
|
+
/** True for one of the four accepted normalized media types. */
|
|
25
|
+
export function isImageMediaType(v) {
|
|
26
|
+
return typeof v === "string" && IMAGE_MEDIA_TYPES.includes(v);
|
|
27
|
+
}
|
|
28
|
+
// Constructors mirror the engine's `impl Message` (same names, camelCase).
|
|
29
|
+
/** `Message::user`. */
|
|
30
|
+
export function userMessage(text) {
|
|
31
|
+
return { role: "user", content: [{ type: "text", content: text }], tool_call_id: null };
|
|
32
|
+
}
|
|
33
|
+
/** `Message::system`. */
|
|
34
|
+
export function systemMessage(text) {
|
|
35
|
+
return { role: "system", content: [{ type: "text", content: text }], tool_call_id: null };
|
|
36
|
+
}
|
|
37
|
+
/** `Message::assistant_text`. */
|
|
38
|
+
export function assistantText(text) {
|
|
39
|
+
return { role: "assistant", content: [{ type: "text", content: text }], tool_call_id: null };
|
|
40
|
+
}
|
|
41
|
+
/** `Message::assistant_tool_call`. */
|
|
42
|
+
export function assistantToolCall(call) {
|
|
43
|
+
return { role: "assistant", content: [{ type: "tool_call", content: call }], tool_call_id: null };
|
|
44
|
+
}
|
|
45
|
+
/** `Message::tool_result`. */
|
|
46
|
+
export function toolResultMessage(id, text) {
|
|
47
|
+
return { role: "tool", content: [{ type: "text", content: text }], tool_call_id: id };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* W804: a user message that carries text plus image references. The text block
|
|
51
|
+
* stays FIRST and the images follow (section 3.5 ordering rule); no-image callers
|
|
52
|
+
* keep using userMessage so the wire shape is byte-identical when there is
|
|
53
|
+
* nothing to attach.
|
|
54
|
+
*/
|
|
55
|
+
export function userMessageWithImages(text, images) {
|
|
56
|
+
const content = [{ type: "text", content: text }];
|
|
57
|
+
for (const ref of images)
|
|
58
|
+
content.push(imageContent(ref));
|
|
59
|
+
return { role: "user", content, tool_call_id: null };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* W804: a tool result that carries the canonical JSON text plus image
|
|
63
|
+
* references (the read_image case, section 6.3). The TEXT remains the value's
|
|
64
|
+
* serde JSON — the model must still see path/dimensions — and the images are
|
|
65
|
+
* separate content blocks.
|
|
66
|
+
*/
|
|
67
|
+
export function toolResultWithImages(id, text, images) {
|
|
68
|
+
const content = [{ type: "text", content: text }];
|
|
69
|
+
for (const ref of images)
|
|
70
|
+
content.push(imageContent(ref));
|
|
71
|
+
return { role: "tool", content, tool_call_id: id };
|
|
72
|
+
}
|
|
73
|
+
/** Static namespace facade: `Message::user(…)` → `Message.user(…)`. */
|
|
74
|
+
export const Message = {
|
|
75
|
+
user: userMessage,
|
|
76
|
+
system: systemMessage,
|
|
77
|
+
assistantText,
|
|
78
|
+
assistantToolCall,
|
|
79
|
+
toolResult: toolResultMessage,
|
|
80
|
+
userWithImages: userMessageWithImages,
|
|
81
|
+
toolResultWithImages,
|
|
82
|
+
};
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Content helpers (the engine pattern-matches; these are the TS equivalents)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
export function isTextContent(c) {
|
|
87
|
+
return c.type === "text";
|
|
88
|
+
}
|
|
89
|
+
export function isToolCallContent(c) {
|
|
90
|
+
return c.type === "tool_call";
|
|
91
|
+
}
|
|
92
|
+
export function isImageContent(c) {
|
|
93
|
+
return c.type === "image";
|
|
94
|
+
}
|
|
95
|
+
/** Wrap one already-content-addressed reference as a model-visible image block. */
|
|
96
|
+
export function imageContent(ref) {
|
|
97
|
+
return { type: "image", content: ref };
|
|
98
|
+
}
|
|
99
|
+
/** The image blocks of a message, in order (references only, never bytes). */
|
|
100
|
+
export function messageImages(m) {
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const c of m.content)
|
|
103
|
+
if (isImageContent(c))
|
|
104
|
+
out.push(c.content);
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/** The tool calls carried by a message (empty for a text-only message). */
|
|
108
|
+
export function messageToolCalls(m) {
|
|
109
|
+
const out = [];
|
|
110
|
+
for (const c of m.content)
|
|
111
|
+
if (isToolCallContent(c))
|
|
112
|
+
out.push(c.content);
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/** The ids of the tool calls carried by a message. */
|
|
116
|
+
export function toolCallIds(m) {
|
|
117
|
+
return messageToolCalls(m).map((tc) => tc.id);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The text blocks of a message, in order. W804: image blocks are deliberately
|
|
121
|
+
* NOT text — they are never concatenated here (and never stringified into a
|
|
122
|
+
* prompt); a consumer that needs the images reads messageImages.
|
|
123
|
+
*/
|
|
124
|
+
export function messageTexts(m) {
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const c of m.content)
|
|
127
|
+
if (isTextContent(c))
|
|
128
|
+
out.push(c.content);
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
/** The single text block of a text-only message, or null. */
|
|
132
|
+
export function messageText(m) {
|
|
133
|
+
const texts = messageTexts(m);
|
|
134
|
+
return texts.length === 1 ? (texts[0] ?? null) : null;
|
|
135
|
+
}
|
|
136
|
+
/** True when the message carries at least one tool call. */
|
|
137
|
+
export function hasToolCalls(m) {
|
|
138
|
+
return m.content.some(isToolCallContent);
|
|
139
|
+
}
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Attachment reference helpers (W804: the log/event side of ImageRef)
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
/**
|
|
145
|
+
* The content-addressing id shape frozen by the contract: sha256, lowercase hex.
|
|
146
|
+
* (contracts/session-event.schema.json $defs.AttachmentRef.attachment_id)
|
|
147
|
+
*/
|
|
148
|
+
const ATTACHMENT_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
149
|
+
/**
|
|
150
|
+
* True when v has the ImageRef shape the event codec and projection accept.
|
|
151
|
+
* name and original are optional and their absence is the normal case.
|
|
152
|
+
*
|
|
153
|
+
* W834 F07 (R3 batch A): the checks below mirror the FROZEN AttachmentRef
|
|
154
|
+
* schema exactly — a 64-lowercase-hex `attachment_id`, and `width`/`height`
|
|
155
|
+
* that are integers >= 1. The old "string + finite number" test let a
|
|
156
|
+
* non-hex id or a zero/negative/fractional dimension through the codec, into
|
|
157
|
+
* the projection and the model-visible history, while the schema rejected it.
|
|
158
|
+
*/
|
|
159
|
+
export function isImageRef(v) {
|
|
160
|
+
if (typeof v !== "object" || v === null)
|
|
161
|
+
return false;
|
|
162
|
+
const r = v;
|
|
163
|
+
return (typeof r["attachment_id"] === "string" &&
|
|
164
|
+
ATTACHMENT_ID_PATTERN.test(r["attachment_id"]) &&
|
|
165
|
+
isImageMediaType(r["media_type"]) &&
|
|
166
|
+
typeof r["width"] === "number" &&
|
|
167
|
+
Number.isInteger(r["width"]) &&
|
|
168
|
+
r["width"] >= 1 &&
|
|
169
|
+
typeof r["height"] === "number" &&
|
|
170
|
+
Number.isInteger(r["height"]) &&
|
|
171
|
+
r["height"] >= 1 &&
|
|
172
|
+
(r["name"] === undefined || typeof r["name"] === "string"));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Field-whitelist normalization of one decoded attachment reference: unknown
|
|
176
|
+
* fields are dropped and absent optionals stay omitted (serde style). Returns
|
|
177
|
+
* null when the value is not an ImageRef at all.
|
|
178
|
+
*/
|
|
179
|
+
export function normalizeImageRef(v) {
|
|
180
|
+
if (!isImageRef(v))
|
|
181
|
+
return null;
|
|
182
|
+
const out = {
|
|
183
|
+
attachment_id: v.attachment_id,
|
|
184
|
+
media_type: v.media_type,
|
|
185
|
+
width: v.width,
|
|
186
|
+
height: v.height,
|
|
187
|
+
};
|
|
188
|
+
if (typeof v.name === "string")
|
|
189
|
+
out.name = v.name;
|
|
190
|
+
if (typeof v.original === "object" && v.original !== null) {
|
|
191
|
+
const o = v.original;
|
|
192
|
+
if (typeof o["width"] === "number" &&
|
|
193
|
+
typeof o["height"] === "number" &&
|
|
194
|
+
typeof o["bytes"] === "number" &&
|
|
195
|
+
typeof o["media_type"] === "string") {
|
|
196
|
+
out.original = { width: o["width"], height: o["height"], bytes: o["bytes"], media_type: o["media_type"] };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The image references carried by a tool_result value (its attachments field,
|
|
203
|
+
* section 6.3). Anything that is not a valid reference is ignored, so a
|
|
204
|
+
* malformed tool value can never smuggle a bogus image into the prompt.
|
|
205
|
+
*/
|
|
206
|
+
export function attachmentRefsOfValue(value) {
|
|
207
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
208
|
+
return [];
|
|
209
|
+
const raw = value["attachments"];
|
|
210
|
+
if (!Array.isArray(raw))
|
|
211
|
+
return [];
|
|
212
|
+
const out = [];
|
|
213
|
+
for (const item of raw) {
|
|
214
|
+
const ref = normalizeImageRef(item);
|
|
215
|
+
if (ref !== null)
|
|
216
|
+
out.push(ref);
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
/** All-zero usage (`Usage::default`). */
|
|
221
|
+
export function zeroUsage() {
|
|
222
|
+
return { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, cache_read: 0, reasoning_tokens: 0 };
|
|
223
|
+
}
|
|
224
|
+
/** `Usage::is_empty` — true when every counter is zero. */
|
|
225
|
+
export function usageIsEmpty(u) {
|
|
226
|
+
return (u.prompt_tokens === 0 &&
|
|
227
|
+
u.completion_tokens === 0 &&
|
|
228
|
+
u.total_tokens === 0 &&
|
|
229
|
+
u.cache_read === 0 &&
|
|
230
|
+
u.reasoning_tokens === 0);
|
|
231
|
+
}
|
|
232
|
+
/** `Usage::add` — per-field sum, returning a new value. */
|
|
233
|
+
export function usageAdd(a, b) {
|
|
234
|
+
return {
|
|
235
|
+
prompt_tokens: a.prompt_tokens + b.prompt_tokens,
|
|
236
|
+
completion_tokens: a.completion_tokens + b.completion_tokens,
|
|
237
|
+
total_tokens: a.total_tokens + b.total_tokens,
|
|
238
|
+
cache_read: a.cache_read + b.cache_read,
|
|
239
|
+
reasoning_tokens: a.reasoning_tokens + b.reasoning_tokens,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/** Sum a list of usages (`Usage +=` in a loop). */
|
|
243
|
+
export function usageSum(usages) {
|
|
244
|
+
let total = zeroUsage();
|
|
245
|
+
for (const u of usages)
|
|
246
|
+
total = usageAdd(total, u);
|
|
247
|
+
return total;
|
|
248
|
+
}
|
|
249
|
+
/** Cache-hit ratio as reported by the statusline (`cache_read / prompt_tokens`). */
|
|
250
|
+
export function cacheHitRatio(u) {
|
|
251
|
+
return u.prompt_tokens === 0 ? 0 : u.cache_read / u.prompt_tokens;
|
|
252
|
+
}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin seam — port of `crates/core/src/plugin.rs`.
|
|
3
|
+
*
|
|
4
|
+
* Everything in the harness is a plugin: the model adapter, the session log,
|
|
5
|
+
* the tool registry, even the agent loop. A plugin mounts itself by providing
|
|
6
|
+
* services (and event listeners) into a [Context]; the composition root decides
|
|
7
|
+
* which plugins exist, `core` never hardcodes a concrete implementation.
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from "./context.js";
|
|
10
|
+
export interface Plugin {
|
|
11
|
+
/** Stable plugin name (the legacy engine defaults to `type_name`; explicit here). */
|
|
12
|
+
name(): string;
|
|
13
|
+
/** Provide services / listen on the context. */
|
|
14
|
+
mount(ctx: Context): void;
|
|
15
|
+
}
|
|
16
|
+
/** Ergonomic constructor for a plugin whose name is a string literal. */
|
|
17
|
+
export declare function definePlugin(name: string, mount: (ctx: Context) => void): Plugin;
|
|
18
|
+
/** Mount every plugin in registration order (later plugins patch earlier ones). */
|
|
19
|
+
export declare function mountPlugins(ctx: Context, plugins: readonly Plugin[]): Context;
|
|
20
|
+
/** The names of the mounted plugins, in mount order. */
|
|
21
|
+
export declare function pluginNames(plugins: readonly Plugin[]): string[];
|
|
22
|
+
/**
|
|
23
|
+
* `NamedRegistry<T>` — named, ordered, replaceable rows: the "patch"
|
|
24
|
+
* primitive. A later row with the same name shadows an earlier one
|
|
25
|
+
* (`get` scans backwards), while `iter` still reports every row.
|
|
26
|
+
*/
|
|
27
|
+
export declare class NamedRegistry<T> {
|
|
28
|
+
private readonly rows;
|
|
29
|
+
insert(name: string, value: T): void;
|
|
30
|
+
/** Last registration wins. */
|
|
31
|
+
get(name: string): T | undefined;
|
|
32
|
+
/** Every row, in registration order (shadowed rows included). */
|
|
33
|
+
entries(): Array<{
|
|
34
|
+
name: string;
|
|
35
|
+
value: T;
|
|
36
|
+
}>;
|
|
37
|
+
get size(): number;
|
|
38
|
+
}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin seam — port of `crates/core/src/plugin.rs`.
|
|
3
|
+
*
|
|
4
|
+
* Everything in the harness is a plugin: the model adapter, the session log,
|
|
5
|
+
* the tool registry, even the agent loop. A plugin mounts itself by providing
|
|
6
|
+
* services (and event listeners) into a [Context]; the composition root decides
|
|
7
|
+
* which plugins exist, `core` never hardcodes a concrete implementation.
|
|
8
|
+
*/
|
|
9
|
+
/** Ergonomic constructor for a plugin whose name is a string literal. */
|
|
10
|
+
export function definePlugin(name, mount) {
|
|
11
|
+
return { name: () => name, mount };
|
|
12
|
+
}
|
|
13
|
+
/** Mount every plugin in registration order (later plugins patch earlier ones). */
|
|
14
|
+
export function mountPlugins(ctx, plugins) {
|
|
15
|
+
for (const p of plugins)
|
|
16
|
+
p.mount(ctx);
|
|
17
|
+
return ctx;
|
|
18
|
+
}
|
|
19
|
+
/** The names of the mounted plugins, in mount order. */
|
|
20
|
+
export function pluginNames(plugins) {
|
|
21
|
+
return plugins.map((p) => p.name());
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `NamedRegistry<T>` — named, ordered, replaceable rows: the "patch"
|
|
25
|
+
* primitive. A later row with the same name shadows an earlier one
|
|
26
|
+
* (`get` scans backwards), while `iter` still reports every row.
|
|
27
|
+
*/
|
|
28
|
+
export class NamedRegistry {
|
|
29
|
+
rows = [];
|
|
30
|
+
insert(name, value) {
|
|
31
|
+
this.rows.push({ name, value });
|
|
32
|
+
}
|
|
33
|
+
/** Last registration wins. */
|
|
34
|
+
get(name) {
|
|
35
|
+
for (let i = this.rows.length - 1; i >= 0; i--) {
|
|
36
|
+
const row = this.rows[i];
|
|
37
|
+
if (row !== undefined && row.name === name)
|
|
38
|
+
return row.value;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
/** Every row, in registration order (shadowed rows included). */
|
|
43
|
+
entries() {
|
|
44
|
+
return this.rows.map((r) => ({ ...r }));
|
|
45
|
+
}
|
|
46
|
+
get size() {
|
|
47
|
+
return this.rows.length;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine's `derive_messages` projection — a 1:1 port of
|
|
3
|
+
* `crates/session/src/log.rs:73-204`.
|
|
4
|
+
*
|
|
5
|
+
* A2 (W746): this algorithm lives in CORE, not in the L1 implementation
|
|
6
|
+
* package. `SessionLog.deriveMessages()` is a seam method, so the projection
|
|
7
|
+
* has to be part of the seam: an implementation that returns `[]` (or that
|
|
8
|
+
* re-implements the rules slightly differently) is a silent history loss, not
|
|
9
|
+
* an "alternative implementation". `@celestea/session` re-exports these
|
|
10
|
+
* functions; backends built with [projectingSessionLog] get them for free.
|
|
11
|
+
*
|
|
12
|
+
* Rules (all of them are contract, each pinned by a unit test):
|
|
13
|
+
* - `TurnStart` / `TurnEnd` are structural markers, never projected;
|
|
14
|
+
* - `ThinkingDelta` is replay-only decoration (W252), never projected;
|
|
15
|
+
* - `ToolCall` rows are ACCUMULATED, not projected: consecutive calls merge
|
|
16
|
+
* into ONE assistant message carrying one `tool_call` content per call
|
|
17
|
+
* (LLM protocols require all tool calls of a turn in a single message);
|
|
18
|
+
* - the accumulator is flushed before any other event, and after the last
|
|
19
|
+
* event;
|
|
20
|
+
* - rows with `parent_id` (W255 run_code sub-calls) stay in the log for
|
|
21
|
+
* audit/replay but never reach the model: both the call and its result are
|
|
22
|
+
* skipped — the outer run_code round trip is all the model sees;
|
|
23
|
+
* - `ToolResult` projects to a tool message whose text is `"Error: {err}"`
|
|
24
|
+
* when the error is a non-empty string, otherwise the serde_json text of
|
|
25
|
+
* the value (`"null"` when absent).
|
|
26
|
+
*
|
|
27
|
+
* `balance_tool_calls` (W267) then makes the projection protocol-valid: every
|
|
28
|
+
* assistant tool_calls message must be followed by one tool message per call.
|
|
29
|
+
*/
|
|
30
|
+
import type { Message, ToolCall } from "./message.js";
|
|
31
|
+
import type { SessionEvent, ToolResultSurface } from "./types.js";
|
|
32
|
+
/** W267 synthetic result text — byte-for-byte the engine's string (b046564). */
|
|
33
|
+
export declare const CANCELLED_TOOL_CALL_TEXT = "Error: tool call was cancelled before execution (no result recorded)";
|
|
34
|
+
/** The model-visible history of a session log (the engine's `derive_messages_from`). */
|
|
35
|
+
export declare function deriveMessagesFrom(events: readonly SessionEvent[]): Message[];
|
|
36
|
+
/**
|
|
37
|
+
* Flush accumulated tool calls as ONE assistant message (`flush_tool_calls`).
|
|
38
|
+
* It drains the accumulator, mirroring the engine's `std::mem::take`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function flushToolCalls(messages: Message[], pending: ToolCall[]): void;
|
|
41
|
+
/**
|
|
42
|
+
* Project one non-tool-call event (`project`). Returns null for the events the
|
|
43
|
+
* model never sees.
|
|
44
|
+
*/
|
|
45
|
+
export declare function projectEvent(event: SessionEvent): Message | null;
|
|
46
|
+
/**
|
|
47
|
+
* The text of a projected ToolResult: error first, else the value with its
|
|
48
|
+
* `surface` applied (absent surface = the value as serde JSON, as before).
|
|
49
|
+
*/
|
|
50
|
+
export declare function toolResultText(error: string | null | undefined, value: unknown, surface?: ToolResultSurface): string;
|
|
51
|
+
/**
|
|
52
|
+
* W267 protocol balance: every assistant `tool_calls` message is followed by
|
|
53
|
+
* one `tool` message per call id. A cancelled/interrupted turn can stop between
|
|
54
|
+
* ToolCall and ToolResult, leaving a dangling call that makes the whole history
|
|
55
|
+
* invalid for OpenAI-compatible upstreams ("insufficient tool messages
|
|
56
|
+
* following tool_calls message"). A synthetic cancelled result is inserted for
|
|
57
|
+
* each unanswered call; the log itself is untouched (audit keeps the truth).
|
|
58
|
+
*
|
|
59
|
+
* UPSTREAM PARITY NOTE: the cursor advance `i = j + inserted + 1` is ported
|
|
60
|
+
* unchanged from `crates/session/src/log.rs:149`, including its quirk — when a
|
|
61
|
+
* call message is fully answered, the message sitting right after its results
|
|
62
|
+
* is skipped by the cursor, so an unbalanced trailing call message in exactly
|
|
63
|
+
* that position is NOT balanced (see `packages/session/src/log/derive.test.ts`:
|
|
64
|
+
* `mirrors the upstream cursor quirk`). Parity with the engine is the contract
|
|
65
|
+
* here; the quirk is reported upstream rather than diverged from silently.
|
|
66
|
+
*/
|
|
67
|
+
export declare function balanceToolCalls(messages: Message[]): void;
|