@frockbot/kernel-agent-loop 0.3.12 → 0.3.13
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/package.json +5 -5
- package/src/agent.ts +20 -0
- package/src/index.test.ts +75 -0
- package/src/index.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/kernel-agent-loop",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
15
|
+
"@frockbot/kernel-contracts": "0.3.13",
|
|
16
16
|
"cordis": "4.0.0-rc.8"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@frockbot/plugin-models": "0.3.
|
|
20
|
-
"@frockbot/plugin-prompt": "0.3.
|
|
21
|
-
"@frockbot/plugin-tools": "0.3.
|
|
19
|
+
"@frockbot/plugin-models": "0.3.13",
|
|
20
|
+
"@frockbot/plugin-prompt": "0.3.13",
|
|
21
|
+
"@frockbot/plugin-tools": "0.3.13",
|
|
22
22
|
"@types/bun": "1.4.0",
|
|
23
23
|
"@types/node": "26.2.0",
|
|
24
24
|
"typescript": "^7.0.2"
|
package/src/agent.ts
CHANGED
|
@@ -107,6 +107,26 @@ declare module "cordis" {
|
|
|
107
107
|
outcome: "completed" | "not-started",
|
|
108
108
|
) => Promise<void>;
|
|
109
109
|
"agent/turn-stopping": (agent: Agent, turn: number) => Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* A step where the model wrote something *and* called tools, raised the
|
|
112
|
+
* moment the assistant message is journaled and before any tool runs.
|
|
113
|
+
*
|
|
114
|
+
* The kernel has no opinion about what that text is for — a Package that
|
|
115
|
+
* gives the Bot a voice does. In the Shell, the only thing a person sees
|
|
116
|
+
* is a `send_to_user` call, so a model that writes "On it — building the
|
|
117
|
+
* countdown applet now." and then goes on to call three tools has said it
|
|
118
|
+
* to nobody: the client draws one bubble per send, and there was no send.
|
|
119
|
+
* The prompt asks for the call, and this is what catches the model that
|
|
120
|
+
* narrates its acknowledgement in text anyway.
|
|
121
|
+
*
|
|
122
|
+
* Serial, and before the tools, so the promoted line lands ahead of the
|
|
123
|
+
* first tool result rather than after the work it was announcing.
|
|
124
|
+
*/
|
|
125
|
+
"agent/assistant-text": (
|
|
126
|
+
agent: Agent,
|
|
127
|
+
text: string,
|
|
128
|
+
position: { turn: number; step: number; requestId: string },
|
|
129
|
+
) => Promise<void>;
|
|
110
130
|
}
|
|
111
131
|
}
|
|
112
132
|
|
package/src/index.test.ts
CHANGED
|
@@ -3057,4 +3057,79 @@ describe("AgentLoop", () => {
|
|
|
3057
3057
|
),
|
|
3058
3058
|
).toHaveLength(1);
|
|
3059
3059
|
});
|
|
3060
|
+
|
|
3061
|
+
// A model that writes an acknowledgement and then calls tools has said
|
|
3062
|
+
// something to the person, and the Package that owns the Bot's voice has to
|
|
3063
|
+
// hear about it *before* the work it was announcing produces results.
|
|
3064
|
+
test("raises assistant text ahead of the tools the same step called", async () => {
|
|
3065
|
+
const provider: LlmProvider = {
|
|
3066
|
+
id: "acknowledging-model",
|
|
3067
|
+
async *stream() {
|
|
3068
|
+
yield { type: "text-delta", text: "On it — building it now." };
|
|
3069
|
+
yield {
|
|
3070
|
+
type: "tool-call",
|
|
3071
|
+
call: { id: "call-1", name: "build", input: {} },
|
|
3072
|
+
};
|
|
3073
|
+
yield { type: "finish", reason: "tool-calls" };
|
|
3074
|
+
},
|
|
3075
|
+
};
|
|
3076
|
+
const order: string[] = [];
|
|
3077
|
+
const root = await mountRuntime(provider, {
|
|
3078
|
+
name: "build",
|
|
3079
|
+
description: "Does the work the acknowledgement announced.",
|
|
3080
|
+
inputSchema: { type: "object" },
|
|
3081
|
+
execute: () => {
|
|
3082
|
+
order.push("tool");
|
|
3083
|
+
return Promise.resolve({ content: "built", isError: true });
|
|
3084
|
+
},
|
|
3085
|
+
});
|
|
3086
|
+
const seen: string[] = [];
|
|
3087
|
+
root.on("agent/assistant-text", async (_agent, text, position) => {
|
|
3088
|
+
order.push("assistant-text");
|
|
3089
|
+
seen.push(`${position.turn}:${position.step}:${text}`);
|
|
3090
|
+
});
|
|
3091
|
+
const handle = await root.agents.create({
|
|
3092
|
+
...allowEffectOptions,
|
|
3093
|
+
botId: "bot-ack",
|
|
3094
|
+
sessionId: "acknowledging-model",
|
|
3095
|
+
provider: provider.id,
|
|
3096
|
+
model: "model-1",
|
|
3097
|
+
});
|
|
3098
|
+
|
|
3099
|
+
handle.agent.send("build me a countdown");
|
|
3100
|
+
await handle.agent.whenIdle();
|
|
3101
|
+
|
|
3102
|
+
expect(seen[0]).toBe("1:1:On it — building it now.");
|
|
3103
|
+
expect(order[0]).toBe("assistant-text");
|
|
3104
|
+
expect(order).toContain("tool");
|
|
3105
|
+
});
|
|
3106
|
+
|
|
3107
|
+
// A step with nothing to say, or one that says everything it has and stops,
|
|
3108
|
+
// raises nothing: the first has no text, and the second's text is the reply.
|
|
3109
|
+
test("raises nothing for a step with no text or no tools", async () => {
|
|
3110
|
+
const provider: LlmProvider = {
|
|
3111
|
+
id: "quiet-model",
|
|
3112
|
+
async *stream() {
|
|
3113
|
+
yield { type: "text-delta", text: "Here is your answer." };
|
|
3114
|
+
yield { type: "finish", reason: "completed" };
|
|
3115
|
+
},
|
|
3116
|
+
};
|
|
3117
|
+
const root = await mountRuntime(provider);
|
|
3118
|
+
let raised = 0;
|
|
3119
|
+
root.on("agent/assistant-text", async () => {
|
|
3120
|
+
raised += 1;
|
|
3121
|
+
});
|
|
3122
|
+
const handle = await root.agents.create({
|
|
3123
|
+
...allowEffectOptions,
|
|
3124
|
+
botId: "bot-quiet",
|
|
3125
|
+
sessionId: "quiet-model",
|
|
3126
|
+
provider: provider.id,
|
|
3127
|
+
model: "model-1",
|
|
3128
|
+
});
|
|
3129
|
+
|
|
3130
|
+
handle.agent.send("answer me");
|
|
3131
|
+
await handle.agent.whenIdle();
|
|
3132
|
+
|
|
3133
|
+
expect(raised).toBe(0);
|
|
3134
|
+
});
|
|
3060
3135
|
});
|
package/src/index.ts
CHANGED
|
@@ -521,6 +521,7 @@ class LoopAgent implements Agent {
|
|
|
521
521
|
await this.session.flush();
|
|
522
522
|
signal.throwIfAborted();
|
|
523
523
|
await this.#notifyModelOutcome(response.request.requestId, "completed");
|
|
524
|
+
await this.#announceAssistantText(response, openTurn, latestStep);
|
|
524
525
|
if (response.toolCalls.length === 0) {
|
|
525
526
|
const shouldStop = await this.#stepShouldStop(
|
|
526
527
|
openTurn,
|
|
@@ -641,6 +642,7 @@ class LoopAgent implements Agent {
|
|
|
641
642
|
await this.session.flush();
|
|
642
643
|
signal.throwIfAborted();
|
|
643
644
|
await this.#notifyModelOutcome(response.request.requestId, "completed");
|
|
645
|
+
await this.#announceAssistantText(response, openTurn, step);
|
|
644
646
|
if (response.toolCalls.length === 0) {
|
|
645
647
|
const shouldStop = await this.#stepShouldStop(
|
|
646
648
|
openTurn,
|
|
@@ -821,6 +823,7 @@ class LoopAgent implements Agent {
|
|
|
821
823
|
await this.session.flush();
|
|
822
824
|
signal.throwIfAborted();
|
|
823
825
|
await this.#notifyModelOutcome(response.request.requestId, "completed");
|
|
826
|
+
await this.#announceAssistantText(response, turn, step);
|
|
824
827
|
|
|
825
828
|
if (response.toolCalls.length === 0) {
|
|
826
829
|
const shouldStop = await this.#stepShouldStop(
|
|
@@ -1372,6 +1375,31 @@ class LoopAgent implements Agent {
|
|
|
1372
1375
|
return endsTurn;
|
|
1373
1376
|
}
|
|
1374
1377
|
|
|
1378
|
+
/**
|
|
1379
|
+
* Raises `agent/assistant-text` for a step that wrote something and then
|
|
1380
|
+
* called tools, so a Package that owns the Bot's voice can do something with
|
|
1381
|
+
* words the model addressed to the person.
|
|
1382
|
+
*
|
|
1383
|
+
* Only that shape. A step with no tool calls ends the Turn on its assistant
|
|
1384
|
+
* message, which every surface already draws; a step with tools and no text
|
|
1385
|
+
* has nothing to say. The narrow case is the one that went missing: text and
|
|
1386
|
+
* tools together, where the text is an acknowledgement and the tools are the
|
|
1387
|
+
* work it was announcing.
|
|
1388
|
+
*/
|
|
1389
|
+
async #announceAssistantText(
|
|
1390
|
+
response: ModelResponse,
|
|
1391
|
+
turn: number,
|
|
1392
|
+
step: number,
|
|
1393
|
+
): Promise<void> {
|
|
1394
|
+
if (response.toolCalls.length === 0) return;
|
|
1395
|
+
if (response.text.trim().length === 0) return;
|
|
1396
|
+
await this.#ctx.serial("agent/assistant-text", this, response.text, {
|
|
1397
|
+
turn,
|
|
1398
|
+
step,
|
|
1399
|
+
requestId: response.request.requestId,
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1375
1403
|
async #stepShouldStop(
|
|
1376
1404
|
turn: number,
|
|
1377
1405
|
step: number,
|