@pix3/agent-bridge 0.2.0 → 0.2.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/README.md +75 -75
- package/dist/cli.js +171 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +140 -0
- package/dist/config.js.map +1 -0
- package/dist/index.js +293 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy.js +45 -0
- package/dist/proxy.js.map +1 -0
- package/dist/sessions.js +517 -0
- package/dist/sessions.js.map +1 -0
- package/dist/wire.js +121 -0
- package/dist/wire.js.map +1 -0
- package/package.json +47 -38
- package/src/cli.ts +195 -195
- package/src/config.ts +179 -179
- package/src/index.ts +350 -350
- package/src/proxy.ts +68 -68
- package/src/sessions.ts +597 -597
- package/src/wire.ts +161 -161
- package/tsconfig.json +16 -16
package/src/wire.ts
CHANGED
|
@@ -1,161 +1,161 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wire types + mapping helpers for the Anthropic Messages-shaped requests that the pix3 editor
|
|
3
|
-
* sends (see pix3's `AnthropicLlmProvider.buildBody`). Everything arrives as untyped JSON, so the
|
|
4
|
-
* shapes here are deliberately loose records with narrow accessor helpers.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface WireToolDefinition {
|
|
8
|
-
readonly name: string;
|
|
9
|
-
readonly description: string;
|
|
10
|
-
readonly input_schema: Record<string, unknown>;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface WireMessage {
|
|
14
|
-
readonly role: 'user' | 'assistant';
|
|
15
|
-
readonly content: string | WireBlock[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type WireBlock = Record<string, unknown>;
|
|
19
|
-
|
|
20
|
-
export interface WireMessagesRequest {
|
|
21
|
-
readonly model: string;
|
|
22
|
-
readonly max_tokens?: number;
|
|
23
|
-
readonly system?: string | WireBlock[];
|
|
24
|
-
readonly messages: WireMessage[];
|
|
25
|
-
readonly tools?: WireToolDefinition[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface WireToolResult {
|
|
29
|
-
readonly toolUseId: string;
|
|
30
|
-
readonly content: string;
|
|
31
|
-
readonly isError: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export class HttpError extends Error {
|
|
35
|
-
readonly status: number;
|
|
36
|
-
constructor(status: number, message: string) {
|
|
37
|
-
super(message);
|
|
38
|
-
this.status = status;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
43
|
-
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
44
|
-
|
|
45
|
-
/** Parse + minimally validate a `/v1/messages` body. Throws {@link HttpError} on bad input. */
|
|
46
|
-
export const parseMessagesRequest = (raw: unknown): WireMessagesRequest => {
|
|
47
|
-
if (!isRecord(raw)) throw new HttpError(400, 'Request body must be a JSON object.');
|
|
48
|
-
if (typeof raw.model !== 'string' || !raw.model) throw new HttpError(400, 'Missing "model".');
|
|
49
|
-
if (!Array.isArray(raw.messages) || raw.messages.length === 0) {
|
|
50
|
-
throw new HttpError(400, 'Missing "messages".');
|
|
51
|
-
}
|
|
52
|
-
for (const message of raw.messages) {
|
|
53
|
-
if (!isRecord(message) || (message.role !== 'user' && message.role !== 'assistant')) {
|
|
54
|
-
throw new HttpError(400, 'Each message needs a user/assistant role.');
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (raw.tools !== undefined) {
|
|
58
|
-
if (!Array.isArray(raw.tools)) throw new HttpError(400, '"tools" must be an array.');
|
|
59
|
-
for (const tool of raw.tools) {
|
|
60
|
-
if (!isRecord(tool) || typeof tool.name !== 'string' || typeof tool.description !== 'string') {
|
|
61
|
-
throw new HttpError(400, 'Each tool needs a name and description.');
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return raw as unknown as WireMessagesRequest;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
/** Flatten the `system` field (string, or Anthropic text blocks) into one plain string. */
|
|
69
|
-
export const systemToText = (system: WireMessagesRequest['system']): string => {
|
|
70
|
-
if (typeof system === 'string') return system;
|
|
71
|
-
if (Array.isArray(system)) {
|
|
72
|
-
return system
|
|
73
|
-
.map(block => (typeof block.text === 'string' ? block.text : ''))
|
|
74
|
-
.join('');
|
|
75
|
-
}
|
|
76
|
-
return '';
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export const contentToBlocks = (content: WireMessage['content']): WireBlock[] =>
|
|
80
|
-
typeof content === 'string' ? [{ type: 'text', text: content }] : content;
|
|
81
|
-
|
|
82
|
-
/** Extract `tool_result` blocks from a message (pix3 sends result content as a plain string). */
|
|
83
|
-
export const extractToolResults = (message: WireMessage): WireToolResult[] => {
|
|
84
|
-
const results: WireToolResult[] = [];
|
|
85
|
-
for (const block of contentToBlocks(message.content)) {
|
|
86
|
-
if (block.type !== 'tool_result') continue;
|
|
87
|
-
const content =
|
|
88
|
-
typeof block.content === 'string'
|
|
89
|
-
? block.content
|
|
90
|
-
: Array.isArray(block.content)
|
|
91
|
-
? block.content
|
|
92
|
-
.map(part => (isRecord(part) && typeof part.text === 'string' ? part.text : ''))
|
|
93
|
-
.join('\n')
|
|
94
|
-
: '';
|
|
95
|
-
results.push({
|
|
96
|
-
toolUseId: typeof block.tool_use_id === 'string' ? block.tool_use_id : '',
|
|
97
|
-
content,
|
|
98
|
-
isError: block.is_error === true,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
return results;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Map user-authored blocks (text/image) to Anthropic user content; tool_result blocks are skipped.
|
|
106
|
-
* pix3's `cache_control` breakpoints are stripped — the Claude Code harness manages its own cache
|
|
107
|
-
* breakpoints, and stacking pix3's on top could exceed the API's 4-breakpoint limit.
|
|
108
|
-
*/
|
|
109
|
-
export const toUserContent = (message: WireMessage): WireBlock[] =>
|
|
110
|
-
contentToBlocks(message.content)
|
|
111
|
-
.filter(block => block.type === 'text' || block.type === 'image')
|
|
112
|
-
.map(block => {
|
|
113
|
-
if ('cache_control' in block) {
|
|
114
|
-
const { cache_control: _dropped, ...rest } = block;
|
|
115
|
-
return rest;
|
|
116
|
-
}
|
|
117
|
-
return block;
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const clip = (text: string, max: number): string =>
|
|
121
|
-
text.length > max ? `${text.slice(0, max)}… [truncated]` : text;
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Render a pix3 conversation history as a plain-text transcript. Used as a degraded recovery path
|
|
125
|
-
* when no live Claude Code session matches the request (bridge restart, edited history, model
|
|
126
|
-
* switch): the transcript becomes the first user message of a fresh session.
|
|
127
|
-
*/
|
|
128
|
-
export const renderTranscript = (messages: readonly WireMessage[]): string => {
|
|
129
|
-
const lines: string[] = [];
|
|
130
|
-
for (const message of messages) {
|
|
131
|
-
const speaker = message.role === 'user' ? 'User' : 'Assistant';
|
|
132
|
-
for (const block of contentToBlocks(message.content)) {
|
|
133
|
-
switch (block.type) {
|
|
134
|
-
case 'text':
|
|
135
|
-
if (typeof block.text === 'string' && block.text.trim()) {
|
|
136
|
-
lines.push(`${speaker}: ${clip(block.text, 6000)}`);
|
|
137
|
-
}
|
|
138
|
-
break;
|
|
139
|
-
case 'image':
|
|
140
|
-
lines.push(`${speaker}: [image omitted]`);
|
|
141
|
-
break;
|
|
142
|
-
case 'tool_use':
|
|
143
|
-
lines.push(
|
|
144
|
-
`Assistant called tool ${String(block.name)}(${clip(JSON.stringify(block.input ?? {}), 2000)})`
|
|
145
|
-
);
|
|
146
|
-
break;
|
|
147
|
-
case 'tool_result': {
|
|
148
|
-
const text =
|
|
149
|
-
typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
150
|
-
lines.push(
|
|
151
|
-
`Tool result${block.is_error === true ? ' (error)' : ''}: ${clip(text, 4000)}`
|
|
152
|
-
);
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
default:
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return lines.join('\n');
|
|
161
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Wire types + mapping helpers for the Anthropic Messages-shaped requests that the pix3 editor
|
|
3
|
+
* sends (see pix3's `AnthropicLlmProvider.buildBody`). Everything arrives as untyped JSON, so the
|
|
4
|
+
* shapes here are deliberately loose records with narrow accessor helpers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface WireToolDefinition {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly description: string;
|
|
10
|
+
readonly input_schema: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface WireMessage {
|
|
14
|
+
readonly role: 'user' | 'assistant';
|
|
15
|
+
readonly content: string | WireBlock[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type WireBlock = Record<string, unknown>;
|
|
19
|
+
|
|
20
|
+
export interface WireMessagesRequest {
|
|
21
|
+
readonly model: string;
|
|
22
|
+
readonly max_tokens?: number;
|
|
23
|
+
readonly system?: string | WireBlock[];
|
|
24
|
+
readonly messages: WireMessage[];
|
|
25
|
+
readonly tools?: WireToolDefinition[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WireToolResult {
|
|
29
|
+
readonly toolUseId: string;
|
|
30
|
+
readonly content: string;
|
|
31
|
+
readonly isError: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class HttpError extends Error {
|
|
35
|
+
readonly status: number;
|
|
36
|
+
constructor(status: number, message: string) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.status = status;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
43
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
44
|
+
|
|
45
|
+
/** Parse + minimally validate a `/v1/messages` body. Throws {@link HttpError} on bad input. */
|
|
46
|
+
export const parseMessagesRequest = (raw: unknown): WireMessagesRequest => {
|
|
47
|
+
if (!isRecord(raw)) throw new HttpError(400, 'Request body must be a JSON object.');
|
|
48
|
+
if (typeof raw.model !== 'string' || !raw.model) throw new HttpError(400, 'Missing "model".');
|
|
49
|
+
if (!Array.isArray(raw.messages) || raw.messages.length === 0) {
|
|
50
|
+
throw new HttpError(400, 'Missing "messages".');
|
|
51
|
+
}
|
|
52
|
+
for (const message of raw.messages) {
|
|
53
|
+
if (!isRecord(message) || (message.role !== 'user' && message.role !== 'assistant')) {
|
|
54
|
+
throw new HttpError(400, 'Each message needs a user/assistant role.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (raw.tools !== undefined) {
|
|
58
|
+
if (!Array.isArray(raw.tools)) throw new HttpError(400, '"tools" must be an array.');
|
|
59
|
+
for (const tool of raw.tools) {
|
|
60
|
+
if (!isRecord(tool) || typeof tool.name !== 'string' || typeof tool.description !== 'string') {
|
|
61
|
+
throw new HttpError(400, 'Each tool needs a name and description.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return raw as unknown as WireMessagesRequest;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Flatten the `system` field (string, or Anthropic text blocks) into one plain string. */
|
|
69
|
+
export const systemToText = (system: WireMessagesRequest['system']): string => {
|
|
70
|
+
if (typeof system === 'string') return system;
|
|
71
|
+
if (Array.isArray(system)) {
|
|
72
|
+
return system
|
|
73
|
+
.map(block => (typeof block.text === 'string' ? block.text : ''))
|
|
74
|
+
.join('');
|
|
75
|
+
}
|
|
76
|
+
return '';
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const contentToBlocks = (content: WireMessage['content']): WireBlock[] =>
|
|
80
|
+
typeof content === 'string' ? [{ type: 'text', text: content }] : content;
|
|
81
|
+
|
|
82
|
+
/** Extract `tool_result` blocks from a message (pix3 sends result content as a plain string). */
|
|
83
|
+
export const extractToolResults = (message: WireMessage): WireToolResult[] => {
|
|
84
|
+
const results: WireToolResult[] = [];
|
|
85
|
+
for (const block of contentToBlocks(message.content)) {
|
|
86
|
+
if (block.type !== 'tool_result') continue;
|
|
87
|
+
const content =
|
|
88
|
+
typeof block.content === 'string'
|
|
89
|
+
? block.content
|
|
90
|
+
: Array.isArray(block.content)
|
|
91
|
+
? block.content
|
|
92
|
+
.map(part => (isRecord(part) && typeof part.text === 'string' ? part.text : ''))
|
|
93
|
+
.join('\n')
|
|
94
|
+
: '';
|
|
95
|
+
results.push({
|
|
96
|
+
toolUseId: typeof block.tool_use_id === 'string' ? block.tool_use_id : '',
|
|
97
|
+
content,
|
|
98
|
+
isError: block.is_error === true,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return results;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Map user-authored blocks (text/image) to Anthropic user content; tool_result blocks are skipped.
|
|
106
|
+
* pix3's `cache_control` breakpoints are stripped — the Claude Code harness manages its own cache
|
|
107
|
+
* breakpoints, and stacking pix3's on top could exceed the API's 4-breakpoint limit.
|
|
108
|
+
*/
|
|
109
|
+
export const toUserContent = (message: WireMessage): WireBlock[] =>
|
|
110
|
+
contentToBlocks(message.content)
|
|
111
|
+
.filter(block => block.type === 'text' || block.type === 'image')
|
|
112
|
+
.map(block => {
|
|
113
|
+
if ('cache_control' in block) {
|
|
114
|
+
const { cache_control: _dropped, ...rest } = block;
|
|
115
|
+
return rest;
|
|
116
|
+
}
|
|
117
|
+
return block;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const clip = (text: string, max: number): string =>
|
|
121
|
+
text.length > max ? `${text.slice(0, max)}… [truncated]` : text;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Render a pix3 conversation history as a plain-text transcript. Used as a degraded recovery path
|
|
125
|
+
* when no live Claude Code session matches the request (bridge restart, edited history, model
|
|
126
|
+
* switch): the transcript becomes the first user message of a fresh session.
|
|
127
|
+
*/
|
|
128
|
+
export const renderTranscript = (messages: readonly WireMessage[]): string => {
|
|
129
|
+
const lines: string[] = [];
|
|
130
|
+
for (const message of messages) {
|
|
131
|
+
const speaker = message.role === 'user' ? 'User' : 'Assistant';
|
|
132
|
+
for (const block of contentToBlocks(message.content)) {
|
|
133
|
+
switch (block.type) {
|
|
134
|
+
case 'text':
|
|
135
|
+
if (typeof block.text === 'string' && block.text.trim()) {
|
|
136
|
+
lines.push(`${speaker}: ${clip(block.text, 6000)}`);
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
case 'image':
|
|
140
|
+
lines.push(`${speaker}: [image omitted]`);
|
|
141
|
+
break;
|
|
142
|
+
case 'tool_use':
|
|
143
|
+
lines.push(
|
|
144
|
+
`Assistant called tool ${String(block.name)}(${clip(JSON.stringify(block.input ?? {}), 2000)})`
|
|
145
|
+
);
|
|
146
|
+
break;
|
|
147
|
+
case 'tool_result': {
|
|
148
|
+
const text =
|
|
149
|
+
typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
150
|
+
lines.push(
|
|
151
|
+
`Tool result${block.is_error === true ? ' (error)' : ''}: ${clip(text, 4000)}`
|
|
152
|
+
);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
default:
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return lines.join('\n');
|
|
161
|
+
};
|
package/tsconfig.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2023",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "nodenext",
|
|
6
|
-
"lib": ["ES2023"],
|
|
7
|
-
"types": ["node"],
|
|
8
|
-
"strict": true,
|
|
9
|
-
"noEmit": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"allowImportingTsExtensions": true,
|
|
12
|
-
"verbatimModuleSyntax": true,
|
|
13
|
-
"erasableSyntaxOnly": true
|
|
14
|
-
},
|
|
15
|
-
"include": ["src"]
|
|
16
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"lib": ["ES2023"],
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"erasableSyntaxOnly": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"]
|
|
16
|
+
}
|