@frockbot/plugin-echo 0.0.0 → 0.1.0
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/frockbot.json +15 -0
- package/package.json +31 -6
- package/src/agent.test.ts +94 -0
- package/src/agent.ts +78 -0
- package/src/index.ts +2 -0
- package/src/manifest.ts +3 -0
- package/tsconfig.json +15 -0
- package/README.md +0 -3
package/frockbot.json
ADDED
package/package.json
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-echo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./agent": "./src/agent.ts",
|
|
9
|
+
"./manifest": "./src/manifest.ts",
|
|
10
|
+
"./frockbot.json": "./frockbot.json",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"frockbot": {
|
|
14
|
+
"manifest": "./frockbot.json"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "bun test src",
|
|
18
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@frockbot/kernel-contracts": "0.1.0",
|
|
22
|
+
"@frockbot/plugin-models": "0.1.0",
|
|
23
|
+
"@frockbot/plugin-tools": "0.1.0",
|
|
24
|
+
"cordis": "4.0.0-rc.8"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@frockbot/plugin-testkit": "0.1.0",
|
|
28
|
+
"@types/bun": "1.4.0",
|
|
29
|
+
"typescript": "^7.0.2"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
6
34
|
"repository": {
|
|
7
35
|
"type": "git",
|
|
8
36
|
"url": "git+https://github.com/timoconnellaus/frockbot.git",
|
|
9
37
|
"directory": "packages/plugin-echo"
|
|
10
|
-
},
|
|
11
|
-
"publishConfig": {
|
|
12
|
-
"access": "public"
|
|
13
38
|
}
|
|
14
39
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
type LlmStreamEvent,
|
|
4
|
+
type NormalizedModelRequest,
|
|
5
|
+
} from "@frockbot/kernel-contracts";
|
|
6
|
+
import { LlmRegistry } from "@frockbot/plugin-models";
|
|
7
|
+
import { ToolRegistry } from "@frockbot/plugin-tools";
|
|
8
|
+
import {
|
|
9
|
+
createPluginHarness,
|
|
10
|
+
verifyPluginPackage,
|
|
11
|
+
} from "@frockbot/plugin-testkit";
|
|
12
|
+
import manifest from "../frockbot.json" with { type: "json" };
|
|
13
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
14
|
+
import echoAgentPlugin, { ECHO_TOOL_NAME } from "./agent.js";
|
|
15
|
+
|
|
16
|
+
function request(content: string): NormalizedModelRequest {
|
|
17
|
+
return {
|
|
18
|
+
requestId: "request",
|
|
19
|
+
provider: "fixture",
|
|
20
|
+
model: "fixture",
|
|
21
|
+
system: "",
|
|
22
|
+
messages: [{ role: "user", content }],
|
|
23
|
+
tools: [],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function collect(
|
|
28
|
+
source: AsyncIterable<LlmStreamEvent>,
|
|
29
|
+
): Promise<LlmStreamEvent[]> {
|
|
30
|
+
const events: LlmStreamEvent[] = [];
|
|
31
|
+
for await (const event of source) events.push(event);
|
|
32
|
+
return events;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("echo plugin", () => {
|
|
36
|
+
test("registers its tool and handles the reference echo command", async () => {
|
|
37
|
+
const harness = await createPluginHarness([LlmRegistry, ToolRegistry]);
|
|
38
|
+
harness.root.llm.register({
|
|
39
|
+
id: "fixture",
|
|
40
|
+
async *stream() {
|
|
41
|
+
yield* [] as LlmStreamEvent[];
|
|
42
|
+
throw new Error("echo middleware delegated unexpectedly");
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const fiber = await harness.mount(echoAgentPlugin);
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
|
|
48
|
+
const events = await collect(
|
|
49
|
+
harness.root.llm.stream(
|
|
50
|
+
request("/echo hello plugins"),
|
|
51
|
+
controller.signal,
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
expect(events).toHaveLength(2);
|
|
55
|
+
expect(events[0]).toMatchObject({
|
|
56
|
+
type: "tool-call",
|
|
57
|
+
call: { name: ECHO_TOOL_NAME, input: { text: "hello plugins" } },
|
|
58
|
+
});
|
|
59
|
+
const first = events[0];
|
|
60
|
+
if (first?.type !== "tool-call") throw new Error("expected a tool call");
|
|
61
|
+
const preparation = await harness.root.tools.prepare(first.call, {
|
|
62
|
+
botId: "echo-bot",
|
|
63
|
+
agentId: "echo-agent",
|
|
64
|
+
compositionGenerationId: "bootstrap",
|
|
65
|
+
turnType: "chat" as const,
|
|
66
|
+
sessionId: "session",
|
|
67
|
+
effectId: "tool:1:1:0",
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
});
|
|
70
|
+
if (preparation.kind !== "ready") throw new Error("echo tool was denied");
|
|
71
|
+
expect(
|
|
72
|
+
await harness.root.tools.executePrepared(preparation, {
|
|
73
|
+
botId: "echo-bot",
|
|
74
|
+
agentId: "echo-agent",
|
|
75
|
+
compositionGenerationId: "bootstrap",
|
|
76
|
+
turnType: "chat" as const,
|
|
77
|
+
sessionId: "session",
|
|
78
|
+
effectId: "tool:1:1:0",
|
|
79
|
+
signal: controller.signal,
|
|
80
|
+
}),
|
|
81
|
+
).toEqual({ content: "hello plugins", isError: false });
|
|
82
|
+
|
|
83
|
+
await fiber.dispose();
|
|
84
|
+
expect(harness.root.tools.schemas({ turnType: "chat" })).toEqual([]);
|
|
85
|
+
await harness.dispose();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("satisfies plugin package conventions", () => {
|
|
89
|
+
expect(verifyPluginPackage({ packageJson, manifest })).toMatchObject({
|
|
90
|
+
name: "@frockbot/plugin-echo",
|
|
91
|
+
contributionKinds: ["runtime"],
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type LlmStreamEvent,
|
|
3
|
+
type NormalizedModelRequest,
|
|
4
|
+
type ToolDefinition,
|
|
5
|
+
} from "@frockbot/kernel-contracts";
|
|
6
|
+
import type { Plugin } from "cordis";
|
|
7
|
+
|
|
8
|
+
export const ECHO_TOOL_NAME = "echo";
|
|
9
|
+
|
|
10
|
+
interface EchoInput {
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function decodeEchoInput(input: unknown): EchoInput | undefined {
|
|
15
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const text = (input as Record<string, unknown>).text;
|
|
19
|
+
if (typeof text !== "string" || !text.trim()) return undefined;
|
|
20
|
+
return { text: text.trim() };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const echoTool: ToolDefinition = {
|
|
24
|
+
name: ECHO_TOOL_NAME,
|
|
25
|
+
description: "Return text to the conversation unchanged.",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: { text: { type: "string" } },
|
|
29
|
+
required: ["text"],
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
},
|
|
32
|
+
idempotent: true,
|
|
33
|
+
validate: (input: unknown) => decodeEchoInput(input) !== undefined,
|
|
34
|
+
execute: async (input: unknown) => {
|
|
35
|
+
const decoded = decodeEchoInput(input);
|
|
36
|
+
if (!decoded) return { content: "Echo text is required", isError: true };
|
|
37
|
+
return { content: decoded.text, isError: false };
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function requestedEchoText(
|
|
42
|
+
request: NormalizedModelRequest,
|
|
43
|
+
): string | undefined {
|
|
44
|
+
if (request.messages.at(-1)?.role === "tool") return undefined;
|
|
45
|
+
const user = request.messages.findLast((message) => message.role === "user");
|
|
46
|
+
if (user?.role !== "user") return undefined;
|
|
47
|
+
const match = /^\/echo(?:\s+([\s\S]+))?$/.exec(user.content.trim());
|
|
48
|
+
return match?.[1]?.trim() || undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function* requestEchoTool(
|
|
52
|
+
text: string,
|
|
53
|
+
signal: AbortSignal,
|
|
54
|
+
): AsyncIterable<LlmStreamEvent> {
|
|
55
|
+
signal.throwIfAborted();
|
|
56
|
+
yield {
|
|
57
|
+
// pi-lens-ignore: ts:2322
|
|
58
|
+
type: "tool-call",
|
|
59
|
+
call: { id: crypto.randomUUID(), name: ECHO_TOOL_NAME, input: { text } },
|
|
60
|
+
};
|
|
61
|
+
yield {
|
|
62
|
+
type: "finish",
|
|
63
|
+
// pi-lens-ignore: ts:2322
|
|
64
|
+
reason: "tool-calls",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const echoAgentPlugin: Plugin.Function = (ctx) => {
|
|
69
|
+
const unregisterTool = ctx.tools.register(echoTool);
|
|
70
|
+
const unregisterMiddleware = ctx.on("llm/stream", (request, signal, next) => {
|
|
71
|
+
const text = requestedEchoText(request);
|
|
72
|
+
return text ? requestEchoTool(text, signal) : next();
|
|
73
|
+
});
|
|
74
|
+
return [unregisterTool, unregisterMiddleware];
|
|
75
|
+
};
|
|
76
|
+
echoAgentPlugin.inject = ["tools", "llm"];
|
|
77
|
+
|
|
78
|
+
export default echoAgentPlugin;
|
package/src/index.ts
ADDED
package/src/manifest.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"allowImportingTsExtensions": true,
|
|
7
|
+
"resolveJsonModule": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"lib": ["ES2023", "DOM"],
|
|
12
|
+
"types": ["bun"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*.ts"]
|
|
15
|
+
}
|
package/README.md
DELETED