@sprucelabs/sprucebot-llm 18.0.1 → 18.1.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 +29 -5
- package/build/bots/SprucebotLlmBotImpl.d.ts +2 -0
- package/build/bots/SprucebotLlmBotImpl.js +6 -1
- package/build/bots/TurnRequest.d.ts +2 -0
- package/build/bots/TurnRequest.js +13 -5
- package/build/bots/adapters/MessageSender.d.ts +1 -0
- package/build/bots/adapters/MessageSender.js +7 -3
- package/build/esm/bots/SprucebotLlmBotImpl.d.ts +2 -0
- package/build/esm/bots/SprucebotLlmBotImpl.js +6 -1
- package/build/esm/bots/TurnRequest.d.ts +2 -0
- package/build/esm/bots/TurnRequest.js +12 -5
- package/build/esm/bots/adapters/MessageSender.d.ts +1 -0
- package/build/esm/bots/adapters/MessageSender.js +7 -3
- package/build/esm/llm.types.d.ts +3 -0
- package/build/esm/parsingResponses/ResponseParserV2.d.ts +6 -0
- package/build/esm/parsingResponses/ResponseParserV2.js +215 -23
- package/build/llm.types.d.ts +3 -0
- package/build/parsingResponses/ResponseParserV2.d.ts +6 -0
- package/build/parsingResponses/ResponseParserV2.js +215 -23
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ Here are the contents of that file for you to review now, rather than needing to
|
|
|
73
73
|
import { stdin as input, stdout as output } from 'node:process'
|
|
74
74
|
import * as readline from 'node:readline/promises'
|
|
75
75
|
import dotenv from 'dotenv'
|
|
76
|
-
import OpenAiAdapter from './bots/adapters/
|
|
76
|
+
import OpenAiAdapter from './bots/adapters/OpenAiAdapter'
|
|
77
77
|
import SprucebotLlmFactory from './bots/SprucebotLlmFactory'
|
|
78
78
|
import buildCallbackSkill from './examples/buildCallbackSkill'
|
|
79
79
|
import buildFileTransformerSkill from './examples/buildFileTransformerSkill'
|
|
@@ -131,7 +131,7 @@ void (async () => {
|
|
|
131
131
|
There are two different limits to be aware of:
|
|
132
132
|
|
|
133
133
|
- `SprucebotLlmBotImpl.messageMemoryLimit` (default: `10`) controls how many messages are kept in the in-memory history on the Bot. Once the limit is hit, old messages are dropped and can no longer be sent to any adapter.
|
|
134
|
-
- `OPENAI_MESSAGE_MEMORY_LIMIT` or `
|
|
134
|
+
- `OPENAI_MESSAGE_MEMORY_LIMIT` or `adapter.setMemoryLimit(limit)` controls how many of those tracked messages are included when sending a request to OpenAI. `0` (the default) means "no additional limit" beyond the Bot history.
|
|
135
135
|
|
|
136
136
|
To change the Bot history limit:
|
|
137
137
|
|
|
@@ -148,6 +148,28 @@ Additional OpenAI context controls:
|
|
|
148
148
|
|
|
149
149
|
## Adapters
|
|
150
150
|
|
|
151
|
+
### Custom request headers
|
|
152
|
+
|
|
153
|
+
You can attach custom HTTP headers to every underlying provider request a bot makes — useful for gateways, request tracing, or provider beta feature flags. Headers are threaded through the Bot's normal `sendMessage(...)` flow, so you set them once on the Bot.
|
|
154
|
+
|
|
155
|
+
Set them when constructing the bot:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
const bot = bots.Bot({
|
|
159
|
+
youAre: 'a helpful assistant',
|
|
160
|
+
skill: mySkill,
|
|
161
|
+
headers: { 'x-my-trace-id': '1234' },
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Or update them at any time (e.g. to rotate a trace id per conversation):
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
bot.setHeaders({ 'x-my-trace-id': '5678' })
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The headers flow through to the underlying OpenAI / Anthropic request on the next `sendMessage`. Adapters also accept them per-call via `SendMessageOptions` if you call `adapter.sendMessage(bot, { model, headers })` directly.
|
|
172
|
+
|
|
151
173
|
### OpenAI adapter configuration
|
|
152
174
|
|
|
153
175
|
Required environment variable:
|
|
@@ -178,7 +200,7 @@ const adapter = OpenAiAdapter.Adapter(process.env.OPEN_AI_API_KEY!, {
|
|
|
178
200
|
|
|
179
201
|
// Or set after creation
|
|
180
202
|
adapter.setModel('gpt-4o')
|
|
181
|
-
adapter.
|
|
203
|
+
adapter.setMemoryLimit(10)
|
|
182
204
|
adapter.setReasoningEffort('low')
|
|
183
205
|
```
|
|
184
206
|
|
|
@@ -193,7 +215,7 @@ adapter.setReasoningEffort('low')
|
|
|
193
215
|
- `reasoningEffort` - for reasoning models (`'low'`, `'medium'`, `'high'`)
|
|
194
216
|
- `baseUrl` - custom API endpoint
|
|
195
217
|
- `adapter.setModel(model)`: set a default model for all requests unless a Skill overrides it.
|
|
196
|
-
- `adapter.
|
|
218
|
+
- `adapter.setMemoryLimit(limit)`: limit how many tracked messages are sent to OpenAI.
|
|
197
219
|
- `adapter.setReasoningEffort(effort)`: set `reasoning_effort` for models that support it.
|
|
198
220
|
- `adapter.getTokenUsage()`: returns cumulative [token usage](#tracking-token-usage) for this adapter. The OpenAI adapter currently returns zeros (not yet implemented).
|
|
199
221
|
- `OpenAiAdapter.OpenAI`: assign a custom OpenAI client class (useful for tests).
|
|
@@ -230,7 +252,7 @@ const bots = SprucebotLlmFactory.Factory(adapter)
|
|
|
230
252
|
|
|
231
253
|
#### Anthropic prompt caching
|
|
232
254
|
|
|
233
|
-
The Anthropic adapter automatically enables [prompt caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching) by inserting an `ephemeral` cache breakpoint after the system prompt. This allows Anthropic to cache the static portion of the prompt (your `youAre` + skill instructions) and only re-process the changing chat history on each turn — reducing latency and cost on long conversations.
|
|
255
|
+
The Anthropic adapter automatically enables [prompt caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching) by inserting an `ephemeral` cache breakpoint (with a `1h` TTL) after the system prompt. This allows Anthropic to cache the static portion of the prompt (your `youAre` + skill instructions) and only re-process the changing chat history on each turn — reducing latency and cost on long conversations.
|
|
234
256
|
|
|
235
257
|
Token usage (including cache creation and cache read tokens) is logged at the `info` level on each request:
|
|
236
258
|
|
|
@@ -689,6 +711,7 @@ skill.unserialize(skillSnapshot)
|
|
|
689
711
|
| `clearMessageHistory()` | Drop all tracked messages |
|
|
690
712
|
| `updateState(partialState)` | Update state and emit `did-update-state` |
|
|
691
713
|
| `setSkill(skill)` | Swap the active skill |
|
|
714
|
+
| `setHeaders(headers)` | Set custom HTTP headers attached to subsequent provider requests |
|
|
692
715
|
| `serialize()` | Snapshot of bot's current state, skill, and history |
|
|
693
716
|
| `unserialize(serialized)` | Restore state from a previous `serialize()` snapshot |
|
|
694
717
|
|
|
@@ -698,6 +721,7 @@ skill.unserialize(skillSnapshot)
|
|
|
698
721
|
|--------|-------------|
|
|
699
722
|
| `updateState(partialState)` | Update skill state |
|
|
700
723
|
| `getState()` | Get current state |
|
|
724
|
+
| `updateJobDescription(prompt)` | Change the skill's job description (`yourJobIfYouChooseToAcceptItIs`) |
|
|
701
725
|
| `setModel(model)` | Change the model this skill uses |
|
|
702
726
|
| `serialize()` | Snapshot of skill configuration and state |
|
|
703
727
|
| `unserialize(serialized)` | Restore skill state from a previous `serialize()` snapshot |
|
|
@@ -11,6 +11,7 @@ export default class SprucebotLlmBotImpl<StateSchema extends Schema = Schema, St
|
|
|
11
11
|
protected messages: LlmMessage[];
|
|
12
12
|
protected skill?: SprucebotLLmSkill;
|
|
13
13
|
private activeTurn?;
|
|
14
|
+
private headers?;
|
|
14
15
|
constructor(options: BotOptions<StateSchema, State>);
|
|
15
16
|
clearMessageHistory(): void;
|
|
16
17
|
markAsDone(): void;
|
|
@@ -25,4 +26,5 @@ export default class SprucebotLlmBotImpl<StateSchema extends Schema = Schema, St
|
|
|
25
26
|
getStateSchema(): StateSchema | undefined;
|
|
26
27
|
getState(): Partial<State> | undefined;
|
|
27
28
|
silentlySetState(state: Partial<State>): void;
|
|
29
|
+
setHeaders(headers: Record<string, string>): void;
|
|
28
30
|
}
|
|
@@ -10,7 +10,7 @@ const InternalStateUpdater_1 = __importDefault(require("./InternalStateUpdater")
|
|
|
10
10
|
const TurnRequest_1 = __importDefault(require("./TurnRequest"));
|
|
11
11
|
class SprucebotLlmBotImpl extends mercury_event_emitter_1.AbstractEventEmitter {
|
|
12
12
|
constructor(options) {
|
|
13
|
-
const { adapter, youAre, stateSchema, state, skill } = options;
|
|
13
|
+
const { adapter, youAre, stateSchema, state, skill, headers } = options;
|
|
14
14
|
super(llm_types_1.llmEventContract);
|
|
15
15
|
this.isDone = false;
|
|
16
16
|
this.messages = [];
|
|
@@ -18,6 +18,7 @@ class SprucebotLlmBotImpl extends mercury_event_emitter_1.AbstractEventEmitter {
|
|
|
18
18
|
this.youAre = youAre;
|
|
19
19
|
this.stateSchema = stateSchema;
|
|
20
20
|
this.skill = skill;
|
|
21
|
+
this.headers = headers;
|
|
21
22
|
this.state = stateSchema
|
|
22
23
|
? {
|
|
23
24
|
...(0, schema_1.defaultSchemaValues)(stateSchema),
|
|
@@ -72,6 +73,7 @@ class SprucebotLlmBotImpl extends mercury_event_emitter_1.AbstractEventEmitter {
|
|
|
72
73
|
setDone: (isDone) => (this.isDone = isDone),
|
|
73
74
|
skill: this.skill,
|
|
74
75
|
trackMessage: this.trackMessage.bind(this),
|
|
76
|
+
headers: this.headers,
|
|
75
77
|
});
|
|
76
78
|
return this.activeTurn.sendMessage(llmMessage, cb);
|
|
77
79
|
}
|
|
@@ -105,6 +107,9 @@ class SprucebotLlmBotImpl extends mercury_event_emitter_1.AbstractEventEmitter {
|
|
|
105
107
|
silentlySetState(state) {
|
|
106
108
|
this.state = state;
|
|
107
109
|
}
|
|
110
|
+
setHeaders(headers) {
|
|
111
|
+
this.headers = headers;
|
|
112
|
+
}
|
|
108
113
|
}
|
|
109
114
|
SprucebotLlmBotImpl.messageMemoryLimit = 10;
|
|
110
115
|
exports.default = SprucebotLlmBotImpl;
|
|
@@ -7,6 +7,7 @@ export default class TurnRequest {
|
|
|
7
7
|
private optionallyUpdateState;
|
|
8
8
|
private bot;
|
|
9
9
|
private isCancelled;
|
|
10
|
+
private headers?;
|
|
10
11
|
constructor(options: {
|
|
11
12
|
trackMessage: (message: LlmMessage) => void;
|
|
12
13
|
setDone: (isDone: boolean) => void;
|
|
@@ -14,6 +15,7 @@ export default class TurnRequest {
|
|
|
14
15
|
skill?: SprucebotLLmSkill;
|
|
15
16
|
adapter: LlmAdapter;
|
|
16
17
|
optionallyUpdateState: (state?: Record<string, any>) => Promise<void>;
|
|
18
|
+
headers?: Record<string, string>;
|
|
17
19
|
});
|
|
18
20
|
cancel(): void;
|
|
19
21
|
sendMessage(llmMessage: LlmMessage, cb?: MessageResponseCallback): Promise<string | null>;
|
|
@@ -8,13 +8,14 @@ const ResponseParserFactory_1 = __importDefault(require("../parsingResponses/Res
|
|
|
8
8
|
class TurnRequest {
|
|
9
9
|
constructor(options) {
|
|
10
10
|
this.isCancelled = false;
|
|
11
|
-
const { trackMessage, skill, bot, adapter, setDone, optionallyUpdateState, } = options;
|
|
11
|
+
const { trackMessage, skill, bot, adapter, setDone, optionallyUpdateState, headers, } = options;
|
|
12
12
|
this.trackMessage = trackMessage;
|
|
13
13
|
this.skill = skill;
|
|
14
14
|
this.adapter = adapter;
|
|
15
15
|
this.setDone = setDone;
|
|
16
16
|
this.bot = bot;
|
|
17
17
|
this.optionallyUpdateState = optionallyUpdateState;
|
|
18
|
+
this.headers = headers;
|
|
18
19
|
}
|
|
19
20
|
cancel() {
|
|
20
21
|
this.isCancelled = true;
|
|
@@ -85,11 +86,18 @@ class TurnRequest {
|
|
|
85
86
|
return parsedMessage;
|
|
86
87
|
}
|
|
87
88
|
async sendMessageToAdapter(model) {
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
let options = undefined;
|
|
90
|
+
if (model) {
|
|
91
|
+
options = {
|
|
90
92
|
model,
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (this.headers) {
|
|
96
|
+
options = options
|
|
97
|
+
? { ...options, headers: this.headers }
|
|
98
|
+
: { headers: this.headers };
|
|
99
|
+
}
|
|
100
|
+
return await this.adapter.sendMessage(this.bot, options);
|
|
93
101
|
}
|
|
94
102
|
async parseResponse(response, callbacks) {
|
|
95
103
|
const parser = ResponseParserFactory_1.default.getInstance();
|
|
@@ -45,16 +45,20 @@ class MessageSenderImpl {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
async send(options) {
|
|
48
|
-
const { abortController, reasoningEffort, ...restOptions } = options;
|
|
48
|
+
const { abortController, reasoningEffort, headers, ...restOptions } = options;
|
|
49
49
|
const params = {
|
|
50
50
|
...restOptions,
|
|
51
51
|
};
|
|
52
52
|
if (reasoningEffort) {
|
|
53
53
|
params.reasoning_effort = reasoningEffort;
|
|
54
54
|
}
|
|
55
|
-
const
|
|
55
|
+
const request = {
|
|
56
56
|
signal: abortController.signal,
|
|
57
|
-
}
|
|
57
|
+
};
|
|
58
|
+
if (headers) {
|
|
59
|
+
request.headers = headers;
|
|
60
|
+
}
|
|
61
|
+
const response = await this.sendHandler(params, request);
|
|
58
62
|
return response;
|
|
59
63
|
}
|
|
60
64
|
}
|
|
@@ -11,6 +11,7 @@ export default class SprucebotLlmBotImpl<StateSchema extends Schema = Schema, St
|
|
|
11
11
|
protected messages: LlmMessage[];
|
|
12
12
|
protected skill?: SprucebotLLmSkill;
|
|
13
13
|
private activeTurn?;
|
|
14
|
+
private headers?;
|
|
14
15
|
constructor(options: BotOptions<StateSchema, State>);
|
|
15
16
|
clearMessageHistory(): void;
|
|
16
17
|
markAsDone(): void;
|
|
@@ -25,4 +26,5 @@ export default class SprucebotLlmBotImpl<StateSchema extends Schema = Schema, St
|
|
|
25
26
|
getStateSchema(): StateSchema | undefined;
|
|
26
27
|
getState(): Partial<State> | undefined;
|
|
27
28
|
silentlySetState(state: Partial<State>): void;
|
|
29
|
+
setHeaders(headers: Record<string, string>): void;
|
|
28
30
|
}
|
|
@@ -14,7 +14,7 @@ import InternalStateUpdater from './InternalStateUpdater.js';
|
|
|
14
14
|
import TurnRequest from './TurnRequest.js';
|
|
15
15
|
class SprucebotLlmBotImpl extends AbstractEventEmitter {
|
|
16
16
|
constructor(options) {
|
|
17
|
-
const { adapter, youAre, stateSchema, state, skill } = options;
|
|
17
|
+
const { adapter, youAre, stateSchema, state, skill, headers } = options;
|
|
18
18
|
super(llmEventContract);
|
|
19
19
|
this.isDone = false;
|
|
20
20
|
this.messages = [];
|
|
@@ -22,6 +22,7 @@ class SprucebotLlmBotImpl extends AbstractEventEmitter {
|
|
|
22
22
|
this.youAre = youAre;
|
|
23
23
|
this.stateSchema = stateSchema;
|
|
24
24
|
this.skill = skill;
|
|
25
|
+
this.headers = headers;
|
|
25
26
|
this.state = stateSchema
|
|
26
27
|
? Object.assign(Object.assign({}, defaultSchemaValues(stateSchema)), state)
|
|
27
28
|
: undefined;
|
|
@@ -77,6 +78,7 @@ class SprucebotLlmBotImpl extends AbstractEventEmitter {
|
|
|
77
78
|
setDone: (isDone) => (this.isDone = isDone),
|
|
78
79
|
skill: this.skill,
|
|
79
80
|
trackMessage: this.trackMessage.bind(this),
|
|
81
|
+
headers: this.headers,
|
|
80
82
|
});
|
|
81
83
|
return this.activeTurn.sendMessage(llmMessage, cb);
|
|
82
84
|
});
|
|
@@ -116,6 +118,9 @@ class SprucebotLlmBotImpl extends AbstractEventEmitter {
|
|
|
116
118
|
silentlySetState(state) {
|
|
117
119
|
this.state = state;
|
|
118
120
|
}
|
|
121
|
+
setHeaders(headers) {
|
|
122
|
+
this.headers = headers;
|
|
123
|
+
}
|
|
119
124
|
}
|
|
120
125
|
SprucebotLlmBotImpl.messageMemoryLimit = 10;
|
|
121
126
|
export default SprucebotLlmBotImpl;
|
|
@@ -7,6 +7,7 @@ export default class TurnRequest {
|
|
|
7
7
|
private optionallyUpdateState;
|
|
8
8
|
private bot;
|
|
9
9
|
private isCancelled;
|
|
10
|
+
private headers?;
|
|
10
11
|
constructor(options: {
|
|
11
12
|
trackMessage: (message: LlmMessage) => void;
|
|
12
13
|
setDone: (isDone: boolean) => void;
|
|
@@ -14,6 +15,7 @@ export default class TurnRequest {
|
|
|
14
15
|
skill?: SprucebotLLmSkill;
|
|
15
16
|
adapter: LlmAdapter;
|
|
16
17
|
optionallyUpdateState: (state?: Record<string, any>) => Promise<void>;
|
|
18
|
+
headers?: Record<string, string>;
|
|
17
19
|
});
|
|
18
20
|
cancel(): void;
|
|
19
21
|
sendMessage(llmMessage: LlmMessage, cb?: MessageResponseCallback): Promise<string | null>;
|
|
@@ -12,13 +12,14 @@ import ResponseParserFactory from '../parsingResponses/ResponseParserFactory.js'
|
|
|
12
12
|
export default class TurnRequest {
|
|
13
13
|
constructor(options) {
|
|
14
14
|
this.isCancelled = false;
|
|
15
|
-
const { trackMessage, skill, bot, adapter, setDone, optionallyUpdateState, } = options;
|
|
15
|
+
const { trackMessage, skill, bot, adapter, setDone, optionallyUpdateState, headers, } = options;
|
|
16
16
|
this.trackMessage = trackMessage;
|
|
17
17
|
this.skill = skill;
|
|
18
18
|
this.adapter = adapter;
|
|
19
19
|
this.setDone = setDone;
|
|
20
20
|
this.bot = bot;
|
|
21
21
|
this.optionallyUpdateState = optionallyUpdateState;
|
|
22
|
+
this.headers = headers;
|
|
22
23
|
}
|
|
23
24
|
cancel() {
|
|
24
25
|
this.isCancelled = true;
|
|
@@ -93,11 +94,17 @@ export default class TurnRequest {
|
|
|
93
94
|
}
|
|
94
95
|
sendMessageToAdapter(model) {
|
|
95
96
|
return __awaiter(this, void 0, void 0, function* () {
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
let options = undefined;
|
|
98
|
+
if (model) {
|
|
99
|
+
options = {
|
|
98
100
|
model,
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (this.headers) {
|
|
104
|
+
options = options
|
|
105
|
+
? Object.assign(Object.assign({}, options), { headers: this.headers }) : { headers: this.headers };
|
|
106
|
+
}
|
|
107
|
+
return yield this.adapter.sendMessage(this.bot, options);
|
|
101
108
|
});
|
|
102
109
|
}
|
|
103
110
|
parseResponse(response, callbacks) {
|
|
@@ -61,14 +61,18 @@ class MessageSenderImpl {
|
|
|
61
61
|
}
|
|
62
62
|
send(options) {
|
|
63
63
|
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
-
const { abortController, reasoningEffort } = options, restOptions = __rest(options, ["abortController", "reasoningEffort"]);
|
|
64
|
+
const { abortController, reasoningEffort, headers } = options, restOptions = __rest(options, ["abortController", "reasoningEffort", "headers"]);
|
|
65
65
|
const params = Object.assign({}, restOptions);
|
|
66
66
|
if (reasoningEffort) {
|
|
67
67
|
params.reasoning_effort = reasoningEffort;
|
|
68
68
|
}
|
|
69
|
-
const
|
|
69
|
+
const request = {
|
|
70
70
|
signal: abortController.signal,
|
|
71
|
-
}
|
|
71
|
+
};
|
|
72
|
+
if (headers) {
|
|
73
|
+
request.headers = headers;
|
|
74
|
+
}
|
|
75
|
+
const response = yield this.sendHandler(params, request);
|
|
72
76
|
return response;
|
|
73
77
|
});
|
|
74
78
|
}
|
package/build/esm/llm.types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface BotOptions<StateSchema extends Schema = Schema, State extends S
|
|
|
5
5
|
adapter: LlmAdapter;
|
|
6
6
|
Class?: new (...opts: any[]) => SprucebotLlmBot<Schema, State>;
|
|
7
7
|
skill?: SprucebotLLmSkill<Schema>;
|
|
8
|
+
headers?: Record<string, string>;
|
|
8
9
|
}
|
|
9
10
|
export interface SprucebotLlmBot<StateSchema extends Schema = Schema, State extends SchemaValues<StateSchema> = SchemaValues<StateSchema>> extends MercuryEventEmitter<LlmEventContract> {
|
|
10
11
|
markAsDone(): void;
|
|
@@ -14,6 +15,7 @@ export interface SprucebotLlmBot<StateSchema extends Schema = Schema, State exte
|
|
|
14
15
|
unserialize(serialized: SerializedBot<StateSchema, State>): void;
|
|
15
16
|
updateState(state: Partial<State>): Promise<void>;
|
|
16
17
|
setSkill(skill: SprucebotLLmSkill<any>): void;
|
|
18
|
+
setHeaders(headers: Record<string, string>): void;
|
|
17
19
|
clearMessageHistory(): void;
|
|
18
20
|
}
|
|
19
21
|
export interface LlmAdapter {
|
|
@@ -41,6 +43,7 @@ export interface PromptOptions<StateSchema extends Schema, State extends SchemaV
|
|
|
41
43
|
}
|
|
42
44
|
export interface SendMessageOptions {
|
|
43
45
|
model?: string;
|
|
46
|
+
headers?: Record<string, string>;
|
|
44
47
|
}
|
|
45
48
|
export interface SerializedBot<StateSchema extends Schema = Schema, State extends SchemaValues<StateSchema> = SchemaValues<StateSchema>> extends PromptOptions<Schema, State> {
|
|
46
49
|
messages: LlmMessage[];
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { ResponseParser, LlmCallbackMap, ParsedResponse } from '../llm.types';
|
|
2
|
+
/**
|
|
3
|
+
* Always produce a serializable error representation for @results.
|
|
4
|
+
* Plain Error becomes its message (never {}). Schema-like errors with enumerable
|
|
5
|
+
* payload fall back to message string for a stable wire format.
|
|
6
|
+
*/
|
|
7
|
+
export declare function serializeCallbackError(err: unknown): string;
|
|
2
8
|
export default class ResponseParserV2 implements ResponseParser {
|
|
3
9
|
parse(response: string, callbacks?: LlmCallbackMap): Promise<ParsedResponse>;
|
|
4
10
|
private invokeCallbacks;
|
|
@@ -9,14 +9,186 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import { DONE_TOKEN } from '../bots/templates.js';
|
|
11
11
|
import validateAndNormalizeCallbackOptions from './validateAndNormalizeCallbackOptions.js';
|
|
12
|
+
const RESERVED_CALLBACK_NAMES = new Set(['updateState', 'results']);
|
|
13
|
+
/**
|
|
14
|
+
* Always produce a serializable error representation for @results.
|
|
15
|
+
* Plain Error becomes its message (never {}). Schema-like errors with enumerable
|
|
16
|
+
* payload fall back to message string for a stable wire format.
|
|
17
|
+
*/
|
|
18
|
+
export function serializeCallbackError(err) {
|
|
19
|
+
if (err instanceof Error) {
|
|
20
|
+
return err.message && err.message.length > 0 ? err.message : 'Error';
|
|
21
|
+
}
|
|
22
|
+
if (typeof err === 'string') {
|
|
23
|
+
return err;
|
|
24
|
+
}
|
|
25
|
+
if (err === null) {
|
|
26
|
+
return 'null';
|
|
27
|
+
}
|
|
28
|
+
if (err === undefined) {
|
|
29
|
+
return 'undefined';
|
|
30
|
+
}
|
|
31
|
+
if (typeof err === 'symbol') {
|
|
32
|
+
return String(err);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const json = JSON.stringify(err);
|
|
36
|
+
if (json && json !== '{}') {
|
|
37
|
+
return json;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (_a) {
|
|
41
|
+
// fall through
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return String(err);
|
|
45
|
+
}
|
|
46
|
+
catch (_b) {
|
|
47
|
+
return 'Unknown error';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Find matching closing brace for a JSON object starting at openIdx ('{'). */
|
|
51
|
+
function findMatchingBrace(text, openIdx) {
|
|
52
|
+
let depth = 0;
|
|
53
|
+
let inString = false;
|
|
54
|
+
let escape = false;
|
|
55
|
+
for (let i = openIdx; i < text.length; i++) {
|
|
56
|
+
const ch = text[i];
|
|
57
|
+
if (inString) {
|
|
58
|
+
if (escape) {
|
|
59
|
+
escape = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (ch === '\\') {
|
|
63
|
+
escape = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (ch === '"') {
|
|
67
|
+
inString = false;
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (ch === '"') {
|
|
72
|
+
inString = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (ch === '{') {
|
|
76
|
+
depth++;
|
|
77
|
+
}
|
|
78
|
+
else if (ch === '}') {
|
|
79
|
+
depth--;
|
|
80
|
+
if (depth === 0) {
|
|
81
|
+
return i;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return -1;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Extract @name(...) call spans. Supports multi-line JSON objects via brace-depth
|
|
89
|
+
* scanning and optional leading/trailing whitespace on the call line.
|
|
90
|
+
*/
|
|
91
|
+
function extractCallbackSpans(message) {
|
|
92
|
+
const spans = [];
|
|
93
|
+
const re = /^[ \t]*@([A-Za-z_]\w*)\(/gm;
|
|
94
|
+
let m;
|
|
95
|
+
while ((m = re.exec(message)) !== null) {
|
|
96
|
+
const name = m[1];
|
|
97
|
+
const start = m.index;
|
|
98
|
+
const openParen = m.index + m[0].length - 1;
|
|
99
|
+
let i = openParen + 1;
|
|
100
|
+
while (i < message.length && /[ \t\r\n]/.test(message[i])) {
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
let argsJson;
|
|
104
|
+
let closeParen;
|
|
105
|
+
if (i < message.length && message[i] === ')') {
|
|
106
|
+
closeParen = i;
|
|
107
|
+
argsJson = undefined;
|
|
108
|
+
}
|
|
109
|
+
else if (i < message.length && message[i] === '{') {
|
|
110
|
+
const braceEnd = findMatchingBrace(message, i);
|
|
111
|
+
if (braceEnd < 0) {
|
|
112
|
+
spans.push({
|
|
113
|
+
name,
|
|
114
|
+
fullMatch: message.slice(start),
|
|
115
|
+
start,
|
|
116
|
+
end: message.length,
|
|
117
|
+
});
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
argsJson = message.slice(i, braceEnd + 1);
|
|
121
|
+
let j = braceEnd + 1;
|
|
122
|
+
while (j < message.length && /[ \t\r\n]/.test(message[j])) {
|
|
123
|
+
j++;
|
|
124
|
+
}
|
|
125
|
+
if (j >= message.length || message[j] !== ')') {
|
|
126
|
+
spans.push({
|
|
127
|
+
name,
|
|
128
|
+
fullMatch: message.slice(start, braceEnd + 1),
|
|
129
|
+
argsJson,
|
|
130
|
+
start,
|
|
131
|
+
end: braceEnd + 1,
|
|
132
|
+
});
|
|
133
|
+
re.lastIndex = braceEnd + 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
closeParen = j;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
re.lastIndex = openParen + 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
let end = closeParen + 1;
|
|
143
|
+
// Include trailing newline after the call when present (common LLM layout)
|
|
144
|
+
if (message[end] === '\r') {
|
|
145
|
+
end++;
|
|
146
|
+
}
|
|
147
|
+
if (message[end] === '\n') {
|
|
148
|
+
end++;
|
|
149
|
+
}
|
|
150
|
+
// Leading newline is part of fullMatch when start points after prior content;
|
|
151
|
+
// also absorb a single leading newline just before the match for strip parity
|
|
152
|
+
// with historic `\n@name(...)\n` fixtures.
|
|
153
|
+
let matchStart = start;
|
|
154
|
+
if (matchStart > 0 && message[matchStart - 1] === '\n') {
|
|
155
|
+
matchStart--;
|
|
156
|
+
if (matchStart > 0 && message[matchStart - 1] === '\r') {
|
|
157
|
+
matchStart--;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
spans.push({
|
|
161
|
+
name,
|
|
162
|
+
fullMatch: message.slice(matchStart, end),
|
|
163
|
+
argsJson,
|
|
164
|
+
start: matchStart,
|
|
165
|
+
end,
|
|
166
|
+
});
|
|
167
|
+
re.lastIndex = end;
|
|
168
|
+
}
|
|
169
|
+
return spans;
|
|
170
|
+
}
|
|
171
|
+
function stripCallFromMessage(message, fullMatch) {
|
|
172
|
+
// Historic behavior: remove the call span and glue surrounding text without
|
|
173
|
+
// injecting separators (trim each side after split).
|
|
174
|
+
return message
|
|
175
|
+
.split(fullMatch)
|
|
176
|
+
.map((s) => s.trim())
|
|
177
|
+
.join('')
|
|
178
|
+
.trim();
|
|
179
|
+
}
|
|
12
180
|
export default class ResponseParserV2 {
|
|
13
181
|
parse(response, callbacks) {
|
|
14
182
|
return __awaiter(this, void 0, void 0, function* () {
|
|
15
183
|
let message = response.replace(DONE_TOKEN, '').trim();
|
|
16
184
|
let state = undefined;
|
|
17
185
|
let callbackResults = undefined;
|
|
18
|
-
|
|
186
|
+
// Enter the pass when any @name( call shape is present (incl. typos)
|
|
187
|
+
// or when a registered callback name appears as a call prefix.
|
|
188
|
+
const hasCallShape = message != null && /^[ \t]*@\w+\(/m.test(message);
|
|
189
|
+
const hasRegisteredName = callbacks != null &&
|
|
19
190
|
Object.keys(callbacks).some((name) => message.includes(`@${name}(`));
|
|
191
|
+
const hasCallbacks = hasCallShape || hasRegisteredName;
|
|
20
192
|
if (hasCallbacks) {
|
|
21
193
|
const { callbackResults: c, message: m } = yield this.invokeCallbacks(message, callbacks);
|
|
22
194
|
callbackResults = c;
|
|
@@ -53,26 +225,36 @@ export default class ResponseParserV2 {
|
|
|
53
225
|
}
|
|
54
226
|
invokeCallbacks(message, callbacks) {
|
|
55
227
|
return __awaiter(this, void 0, void 0, function* () {
|
|
228
|
+
var _a;
|
|
56
229
|
let callbackStrippedMessage = message;
|
|
57
230
|
let callbackResults = '';
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
231
|
+
const spans = extractCallbackSpans(message);
|
|
232
|
+
for (const span of spans) {
|
|
233
|
+
if (RESERVED_CALLBACK_NAMES.has(span.name)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const name = span.name;
|
|
237
|
+
// Always strip the call from the user-facing message once recognized.
|
|
238
|
+
callbackStrippedMessage = stripCallFromMessage(callbackStrippedMessage, span.fullMatch);
|
|
239
|
+
let options;
|
|
240
|
+
try {
|
|
241
|
+
options = span.argsJson ? JSON.parse(span.argsJson) : undefined;
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
callbackResults += this.renderCallbackResults({
|
|
245
|
+
name,
|
|
246
|
+
error: `Invalid JSON arguments for @${name}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err)}. Arguments must be valid JSON object.`,
|
|
247
|
+
});
|
|
62
248
|
continue;
|
|
63
249
|
}
|
|
64
|
-
const parsed = match[2] ? JSON.parse(match[2]) : undefined;
|
|
65
|
-
const name = match[1];
|
|
66
|
-
const options = parsed;
|
|
67
250
|
const callback = callbacks === null || callbacks === void 0 ? void 0 : callbacks[name];
|
|
68
251
|
if (!callback) {
|
|
252
|
+
callbackResults += this.renderCallbackResults({
|
|
253
|
+
name,
|
|
254
|
+
error: `Unknown callback @${name} — not registered.`,
|
|
255
|
+
});
|
|
69
256
|
continue;
|
|
70
257
|
}
|
|
71
|
-
const parts = callbackStrippedMessage.split(match[0]);
|
|
72
|
-
callbackStrippedMessage = parts
|
|
73
|
-
.map((s) => s.trim())
|
|
74
|
-
.join('')
|
|
75
|
-
.trim();
|
|
76
258
|
try {
|
|
77
259
|
if (callback === null || callback === void 0 ? void 0 : callback.parameters) {
|
|
78
260
|
validateAndNormalizeCallbackOptions(callback.parameters, options);
|
|
@@ -89,7 +271,7 @@ export default class ResponseParserV2 {
|
|
|
89
271
|
}
|
|
90
272
|
callbackResults = callbackResults.trim();
|
|
91
273
|
return {
|
|
92
|
-
callbackResults,
|
|
274
|
+
callbackResults: callbackResults.length > 0 ? callbackResults : undefined,
|
|
93
275
|
message: callbackStrippedMessage.length > 0
|
|
94
276
|
? callbackStrippedMessage
|
|
95
277
|
: null,
|
|
@@ -97,35 +279,45 @@ export default class ResponseParserV2 {
|
|
|
97
279
|
});
|
|
98
280
|
}
|
|
99
281
|
renderCallbackResults(callbackOptions) {
|
|
100
|
-
|
|
282
|
+
const { name, results, error } = callbackOptions;
|
|
283
|
+
if (error !== undefined) {
|
|
284
|
+
return `@results ${JSON.stringify({
|
|
285
|
+
name,
|
|
286
|
+
error: serializeCallbackError(error),
|
|
287
|
+
})}\n`;
|
|
288
|
+
}
|
|
289
|
+
// Multiline string results: header only + raw body (real newlines).
|
|
290
|
+
// Trailing newline separates blocks when multiple callbacks run.
|
|
291
|
+
if (typeof results === 'string') {
|
|
292
|
+
return `@results ${JSON.stringify({ name })}\n${results}\n`;
|
|
293
|
+
}
|
|
294
|
+
// undefined results: omit results key (historic wire shape).
|
|
295
|
+
if (results === undefined) {
|
|
296
|
+
return `@results ${JSON.stringify({ name })}\n`;
|
|
297
|
+
}
|
|
298
|
+
return `@results ${JSON.stringify({ name, results })}\n`;
|
|
101
299
|
}
|
|
102
300
|
getStateUpdateInstructions() {
|
|
103
301
|
return `Updating state works similar to all function calls. Use the following syntax:
|
|
104
302
|
@updateState({ "field1": "value1", "field2": "value2" })
|
|
105
|
-
Make sure to json encode only the fields you want to change. You can update state once and do it at the end of any messages you send.
|
|
303
|
+
Make sure to json encode only the fields you want to change. You can update state once and do it at the end of any messages you send. Multi-line JSON objects are accepted (pretty-printed objects are parsed). IMPORTANT: Prefer a single JSON object argument; do not omit braces.
|
|
106
304
|
Your user-facing message is always sent to the user, even if @updateState fails. If @updateState fails later, do not repeat the same message. Only send the specific @updateState needed to fix the missing state change.
|
|
107
305
|
Good example:
|
|
108
306
|
@updateState({ "favoriteColor": "blue", "firstName": "Taylor" })
|
|
109
307
|
Bad examples:
|
|
110
308
|
@updateState
|
|
111
309
|
{ "favoriteColor": "blue" }
|
|
112
|
-
@updateState({
|
|
113
|
-
"favoriteColor": "blue"
|
|
114
|
-
})
|
|
115
310
|
@updateState({ favoriteColor: "blue" })`;
|
|
116
311
|
}
|
|
117
312
|
getFunctionCallInstructions() {
|
|
118
313
|
return `A function call is done using the following syntax:
|
|
119
314
|
@callbackName({ "key": "value" })
|
|
120
|
-
Make sure to json encode the options. You can call as many callbacks as you want in a single response by including multiple @functionName() lines.
|
|
315
|
+
Make sure to json encode the options. You can call as many callbacks as you want in a single response by including multiple @functionName() lines. Multi-line JSON arguments are accepted (pretty-printed objects are parsed). Failed callbacks always return a serializable error string in @results — never silence failures and never return an empty error object.
|
|
121
316
|
Your user-facing message is always sent to the user, even if a callback fails. Successful callbacks have already run successfully. If a callback fails later, do not repeat the same message and do not repeat successful callbacks. Only call the specific callback needed to fix the failed gap.
|
|
122
317
|
Good example:
|
|
123
318
|
@lookupWeather({ "zip": "80524" })
|
|
124
319
|
Bad examples:
|
|
125
320
|
@lookupWeather { "zip": "80524" }
|
|
126
|
-
@lookupWeather(
|
|
127
|
-
{ "zip": "80524" }
|
|
128
|
-
)
|
|
129
321
|
@lookupWeather({ zip: "80524" })`;
|
|
130
322
|
}
|
|
131
323
|
}
|
package/build/llm.types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface BotOptions<StateSchema extends Schema = Schema, State extends S
|
|
|
5
5
|
adapter: LlmAdapter;
|
|
6
6
|
Class?: new (...opts: any[]) => SprucebotLlmBot<Schema, State>;
|
|
7
7
|
skill?: SprucebotLLmSkill<Schema>;
|
|
8
|
+
headers?: Record<string, string>;
|
|
8
9
|
}
|
|
9
10
|
export interface SprucebotLlmBot<StateSchema extends Schema = Schema, State extends SchemaValues<StateSchema> = SchemaValues<StateSchema>> extends MercuryEventEmitter<LlmEventContract> {
|
|
10
11
|
markAsDone(): void;
|
|
@@ -14,6 +15,7 @@ export interface SprucebotLlmBot<StateSchema extends Schema = Schema, State exte
|
|
|
14
15
|
unserialize(serialized: SerializedBot<StateSchema, State>): void;
|
|
15
16
|
updateState(state: Partial<State>): Promise<void>;
|
|
16
17
|
setSkill(skill: SprucebotLLmSkill<any>): void;
|
|
18
|
+
setHeaders(headers: Record<string, string>): void;
|
|
17
19
|
clearMessageHistory(): void;
|
|
18
20
|
}
|
|
19
21
|
export interface LlmAdapter {
|
|
@@ -41,6 +43,7 @@ export interface PromptOptions<StateSchema extends Schema, State extends SchemaV
|
|
|
41
43
|
}
|
|
42
44
|
export interface SendMessageOptions {
|
|
43
45
|
model?: string;
|
|
46
|
+
headers?: Record<string, string>;
|
|
44
47
|
}
|
|
45
48
|
export interface SerializedBot<StateSchema extends Schema = Schema, State extends SchemaValues<StateSchema> = SchemaValues<StateSchema>> extends PromptOptions<Schema, State> {
|
|
46
49
|
messages: LlmMessage[];
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { ResponseParser, LlmCallbackMap, ParsedResponse } from '../llm.types';
|
|
2
|
+
/**
|
|
3
|
+
* Always produce a serializable error representation for @results.
|
|
4
|
+
* Plain Error becomes its message (never {}). Schema-like errors with enumerable
|
|
5
|
+
* payload fall back to message string for a stable wire format.
|
|
6
|
+
*/
|
|
7
|
+
export declare function serializeCallbackError(err: unknown): string;
|
|
2
8
|
export default class ResponseParserV2 implements ResponseParser {
|
|
3
9
|
parse(response: string, callbacks?: LlmCallbackMap): Promise<ParsedResponse>;
|
|
4
10
|
private invokeCallbacks;
|
|
@@ -3,15 +3,188 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.serializeCallbackError = serializeCallbackError;
|
|
6
7
|
const templates_1 = require("../bots/templates");
|
|
7
8
|
const validateAndNormalizeCallbackOptions_1 = __importDefault(require("./validateAndNormalizeCallbackOptions"));
|
|
9
|
+
const RESERVED_CALLBACK_NAMES = new Set(['updateState', 'results']);
|
|
10
|
+
/**
|
|
11
|
+
* Always produce a serializable error representation for @results.
|
|
12
|
+
* Plain Error becomes its message (never {}). Schema-like errors with enumerable
|
|
13
|
+
* payload fall back to message string for a stable wire format.
|
|
14
|
+
*/
|
|
15
|
+
function serializeCallbackError(err) {
|
|
16
|
+
if (err instanceof Error) {
|
|
17
|
+
return err.message && err.message.length > 0 ? err.message : 'Error';
|
|
18
|
+
}
|
|
19
|
+
if (typeof err === 'string') {
|
|
20
|
+
return err;
|
|
21
|
+
}
|
|
22
|
+
if (err === null) {
|
|
23
|
+
return 'null';
|
|
24
|
+
}
|
|
25
|
+
if (err === undefined) {
|
|
26
|
+
return 'undefined';
|
|
27
|
+
}
|
|
28
|
+
if (typeof err === 'symbol') {
|
|
29
|
+
return String(err);
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const json = JSON.stringify(err);
|
|
33
|
+
if (json && json !== '{}') {
|
|
34
|
+
return json;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// fall through
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return String(err);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return 'Unknown error';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Find matching closing brace for a JSON object starting at openIdx ('{'). */
|
|
48
|
+
function findMatchingBrace(text, openIdx) {
|
|
49
|
+
let depth = 0;
|
|
50
|
+
let inString = false;
|
|
51
|
+
let escape = false;
|
|
52
|
+
for (let i = openIdx; i < text.length; i++) {
|
|
53
|
+
const ch = text[i];
|
|
54
|
+
if (inString) {
|
|
55
|
+
if (escape) {
|
|
56
|
+
escape = false;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (ch === '\\') {
|
|
60
|
+
escape = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (ch === '"') {
|
|
64
|
+
inString = false;
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (ch === '"') {
|
|
69
|
+
inString = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (ch === '{') {
|
|
73
|
+
depth++;
|
|
74
|
+
}
|
|
75
|
+
else if (ch === '}') {
|
|
76
|
+
depth--;
|
|
77
|
+
if (depth === 0) {
|
|
78
|
+
return i;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return -1;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Extract @name(...) call spans. Supports multi-line JSON objects via brace-depth
|
|
86
|
+
* scanning and optional leading/trailing whitespace on the call line.
|
|
87
|
+
*/
|
|
88
|
+
function extractCallbackSpans(message) {
|
|
89
|
+
const spans = [];
|
|
90
|
+
const re = /^[ \t]*@([A-Za-z_]\w*)\(/gm;
|
|
91
|
+
let m;
|
|
92
|
+
while ((m = re.exec(message)) !== null) {
|
|
93
|
+
const name = m[1];
|
|
94
|
+
const start = m.index;
|
|
95
|
+
const openParen = m.index + m[0].length - 1;
|
|
96
|
+
let i = openParen + 1;
|
|
97
|
+
while (i < message.length && /[ \t\r\n]/.test(message[i])) {
|
|
98
|
+
i++;
|
|
99
|
+
}
|
|
100
|
+
let argsJson;
|
|
101
|
+
let closeParen;
|
|
102
|
+
if (i < message.length && message[i] === ')') {
|
|
103
|
+
closeParen = i;
|
|
104
|
+
argsJson = undefined;
|
|
105
|
+
}
|
|
106
|
+
else if (i < message.length && message[i] === '{') {
|
|
107
|
+
const braceEnd = findMatchingBrace(message, i);
|
|
108
|
+
if (braceEnd < 0) {
|
|
109
|
+
spans.push({
|
|
110
|
+
name,
|
|
111
|
+
fullMatch: message.slice(start),
|
|
112
|
+
start,
|
|
113
|
+
end: message.length,
|
|
114
|
+
});
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
argsJson = message.slice(i, braceEnd + 1);
|
|
118
|
+
let j = braceEnd + 1;
|
|
119
|
+
while (j < message.length && /[ \t\r\n]/.test(message[j])) {
|
|
120
|
+
j++;
|
|
121
|
+
}
|
|
122
|
+
if (j >= message.length || message[j] !== ')') {
|
|
123
|
+
spans.push({
|
|
124
|
+
name,
|
|
125
|
+
fullMatch: message.slice(start, braceEnd + 1),
|
|
126
|
+
argsJson,
|
|
127
|
+
start,
|
|
128
|
+
end: braceEnd + 1,
|
|
129
|
+
});
|
|
130
|
+
re.lastIndex = braceEnd + 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
closeParen = j;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
re.lastIndex = openParen + 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
let end = closeParen + 1;
|
|
140
|
+
// Include trailing newline after the call when present (common LLM layout)
|
|
141
|
+
if (message[end] === '\r') {
|
|
142
|
+
end++;
|
|
143
|
+
}
|
|
144
|
+
if (message[end] === '\n') {
|
|
145
|
+
end++;
|
|
146
|
+
}
|
|
147
|
+
// Leading newline is part of fullMatch when start points after prior content;
|
|
148
|
+
// also absorb a single leading newline just before the match for strip parity
|
|
149
|
+
// with historic `\n@name(...)\n` fixtures.
|
|
150
|
+
let matchStart = start;
|
|
151
|
+
if (matchStart > 0 && message[matchStart - 1] === '\n') {
|
|
152
|
+
matchStart--;
|
|
153
|
+
if (matchStart > 0 && message[matchStart - 1] === '\r') {
|
|
154
|
+
matchStart--;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
spans.push({
|
|
158
|
+
name,
|
|
159
|
+
fullMatch: message.slice(matchStart, end),
|
|
160
|
+
argsJson,
|
|
161
|
+
start: matchStart,
|
|
162
|
+
end,
|
|
163
|
+
});
|
|
164
|
+
re.lastIndex = end;
|
|
165
|
+
}
|
|
166
|
+
return spans;
|
|
167
|
+
}
|
|
168
|
+
function stripCallFromMessage(message, fullMatch) {
|
|
169
|
+
// Historic behavior: remove the call span and glue surrounding text without
|
|
170
|
+
// injecting separators (trim each side after split).
|
|
171
|
+
return message
|
|
172
|
+
.split(fullMatch)
|
|
173
|
+
.map((s) => s.trim())
|
|
174
|
+
.join('')
|
|
175
|
+
.trim();
|
|
176
|
+
}
|
|
8
177
|
class ResponseParserV2 {
|
|
9
178
|
async parse(response, callbacks) {
|
|
10
179
|
let message = response.replace(templates_1.DONE_TOKEN, '').trim();
|
|
11
180
|
let state = undefined;
|
|
12
181
|
let callbackResults = undefined;
|
|
13
|
-
|
|
182
|
+
// Enter the pass when any @name( call shape is present (incl. typos)
|
|
183
|
+
// or when a registered callback name appears as a call prefix.
|
|
184
|
+
const hasCallShape = message != null && /^[ \t]*@\w+\(/m.test(message);
|
|
185
|
+
const hasRegisteredName = callbacks != null &&
|
|
14
186
|
Object.keys(callbacks).some((name) => message.includes(`@${name}(`));
|
|
187
|
+
const hasCallbacks = hasCallShape || hasRegisteredName;
|
|
15
188
|
if (hasCallbacks) {
|
|
16
189
|
const { callbackResults: c, message: m } = await this.invokeCallbacks(message, callbacks);
|
|
17
190
|
callbackResults = c;
|
|
@@ -48,24 +221,33 @@ class ResponseParserV2 {
|
|
|
48
221
|
async invokeCallbacks(message, callbacks) {
|
|
49
222
|
let callbackStrippedMessage = message;
|
|
50
223
|
let callbackResults = '';
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
224
|
+
const spans = extractCallbackSpans(message);
|
|
225
|
+
for (const span of spans) {
|
|
226
|
+
if (RESERVED_CALLBACK_NAMES.has(span.name)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const name = span.name;
|
|
230
|
+
// Always strip the call from the user-facing message once recognized.
|
|
231
|
+
callbackStrippedMessage = stripCallFromMessage(callbackStrippedMessage, span.fullMatch);
|
|
232
|
+
let options;
|
|
233
|
+
try {
|
|
234
|
+
options = span.argsJson ? JSON.parse(span.argsJson) : undefined;
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
callbackResults += this.renderCallbackResults({
|
|
238
|
+
name,
|
|
239
|
+
error: `Invalid JSON arguments for @${name}: ${err?.message ?? String(err)}. Arguments must be valid JSON object.`,
|
|
240
|
+
});
|
|
55
241
|
continue;
|
|
56
242
|
}
|
|
57
|
-
const parsed = match[2] ? JSON.parse(match[2]) : undefined;
|
|
58
|
-
const name = match[1];
|
|
59
|
-
const options = parsed;
|
|
60
243
|
const callback = callbacks?.[name];
|
|
61
244
|
if (!callback) {
|
|
245
|
+
callbackResults += this.renderCallbackResults({
|
|
246
|
+
name,
|
|
247
|
+
error: `Unknown callback @${name} — not registered.`,
|
|
248
|
+
});
|
|
62
249
|
continue;
|
|
63
250
|
}
|
|
64
|
-
const parts = callbackStrippedMessage.split(match[0]);
|
|
65
|
-
callbackStrippedMessage = parts
|
|
66
|
-
.map((s) => s.trim())
|
|
67
|
-
.join('')
|
|
68
|
-
.trim();
|
|
69
251
|
try {
|
|
70
252
|
if (callback?.parameters) {
|
|
71
253
|
(0, validateAndNormalizeCallbackOptions_1.default)(callback.parameters, options);
|
|
@@ -82,42 +264,52 @@ class ResponseParserV2 {
|
|
|
82
264
|
}
|
|
83
265
|
callbackResults = callbackResults.trim();
|
|
84
266
|
return {
|
|
85
|
-
callbackResults,
|
|
267
|
+
callbackResults: callbackResults.length > 0 ? callbackResults : undefined,
|
|
86
268
|
message: callbackStrippedMessage.length > 0
|
|
87
269
|
? callbackStrippedMessage
|
|
88
270
|
: null,
|
|
89
271
|
};
|
|
90
272
|
}
|
|
91
273
|
renderCallbackResults(callbackOptions) {
|
|
92
|
-
|
|
274
|
+
const { name, results, error } = callbackOptions;
|
|
275
|
+
if (error !== undefined) {
|
|
276
|
+
return `@results ${JSON.stringify({
|
|
277
|
+
name,
|
|
278
|
+
error: serializeCallbackError(error),
|
|
279
|
+
})}\n`;
|
|
280
|
+
}
|
|
281
|
+
// Multiline string results: header only + raw body (real newlines).
|
|
282
|
+
// Trailing newline separates blocks when multiple callbacks run.
|
|
283
|
+
if (typeof results === 'string') {
|
|
284
|
+
return `@results ${JSON.stringify({ name })}\n${results}\n`;
|
|
285
|
+
}
|
|
286
|
+
// undefined results: omit results key (historic wire shape).
|
|
287
|
+
if (results === undefined) {
|
|
288
|
+
return `@results ${JSON.stringify({ name })}\n`;
|
|
289
|
+
}
|
|
290
|
+
return `@results ${JSON.stringify({ name, results })}\n`;
|
|
93
291
|
}
|
|
94
292
|
getStateUpdateInstructions() {
|
|
95
293
|
return `Updating state works similar to all function calls. Use the following syntax:
|
|
96
294
|
@updateState({ "field1": "value1", "field2": "value2" })
|
|
97
|
-
Make sure to json encode only the fields you want to change. You can update state once and do it at the end of any messages you send.
|
|
295
|
+
Make sure to json encode only the fields you want to change. You can update state once and do it at the end of any messages you send. Multi-line JSON objects are accepted (pretty-printed objects are parsed). IMPORTANT: Prefer a single JSON object argument; do not omit braces.
|
|
98
296
|
Your user-facing message is always sent to the user, even if @updateState fails. If @updateState fails later, do not repeat the same message. Only send the specific @updateState needed to fix the missing state change.
|
|
99
297
|
Good example:
|
|
100
298
|
@updateState({ "favoriteColor": "blue", "firstName": "Taylor" })
|
|
101
299
|
Bad examples:
|
|
102
300
|
@updateState
|
|
103
301
|
{ "favoriteColor": "blue" }
|
|
104
|
-
@updateState({
|
|
105
|
-
"favoriteColor": "blue"
|
|
106
|
-
})
|
|
107
302
|
@updateState({ favoriteColor: "blue" })`;
|
|
108
303
|
}
|
|
109
304
|
getFunctionCallInstructions() {
|
|
110
305
|
return `A function call is done using the following syntax:
|
|
111
306
|
@callbackName({ "key": "value" })
|
|
112
|
-
Make sure to json encode the options. You can call as many callbacks as you want in a single response by including multiple @functionName() lines.
|
|
307
|
+
Make sure to json encode the options. You can call as many callbacks as you want in a single response by including multiple @functionName() lines. Multi-line JSON arguments are accepted (pretty-printed objects are parsed). Failed callbacks always return a serializable error string in @results — never silence failures and never return an empty error object.
|
|
113
308
|
Your user-facing message is always sent to the user, even if a callback fails. Successful callbacks have already run successfully. If a callback fails later, do not repeat the same message and do not repeat successful callbacks. Only call the specific callback needed to fix the failed gap.
|
|
114
309
|
Good example:
|
|
115
310
|
@lookupWeather({ "zip": "80524" })
|
|
116
311
|
Bad examples:
|
|
117
312
|
@lookupWeather { "zip": "80524" }
|
|
118
|
-
@lookupWeather(
|
|
119
|
-
{ "zip": "80524" }
|
|
120
|
-
)
|
|
121
313
|
@lookupWeather({ zip: "80524" })`;
|
|
122
314
|
}
|
|
123
315
|
}
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"eta"
|
|
9
9
|
]
|
|
10
10
|
},
|
|
11
|
-
"version": "18.
|
|
11
|
+
"version": "18.1.1",
|
|
12
12
|
"files": [
|
|
13
13
|
"build"
|
|
14
14
|
],
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"types": "./build/index.d.ts",
|
|
27
27
|
"default": "./build/index.js"
|
|
28
28
|
}
|
|
29
|
-
}
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
30
31
|
},
|
|
31
32
|
"keywords": [
|
|
32
33
|
"llm",
|