@frockbot/plugin-shell 0.0.0 → 0.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/frockbot.json +68 -0
- package/package.json +87 -6
- package/src/agent.test.ts +372 -0
- package/src/agent.ts +335 -0
- package/src/approvals.test.ts +224 -0
- package/src/approvals.ts +530 -0
- package/src/backend-assignment.test.ts +161 -0
- package/src/backend-assignment.ts +274 -0
- package/src/backend-authoring.test.ts +518 -0
- package/src/backend-authoring.ts +531 -0
- package/src/backend-bot-identity.test.ts +215 -0
- package/src/backend-completion.test.ts +289 -0
- package/src/backend-completion.ts +95 -0
- package/src/backend-composition.ts +242 -0
- package/src/backend-computer.ts +76 -0
- package/src/backend-configuration.test.ts +1757 -0
- package/src/backend-contracts.test.ts +189 -0
- package/src/backend-contracts.ts +44 -0
- package/src/backend-debug.test.ts +202 -0
- package/src/backend-execution.ts +55 -0
- package/src/backend-flock.ts +96 -0
- package/src/backend-image.test.ts +115 -0
- package/src/backend-image.ts +180 -0
- package/src/backend-isolate.test.ts +238 -0
- package/src/backend-isolate.ts +409 -0
- package/src/backend-machine.ts +144 -0
- package/src/backend-memory.ts +89 -0
- package/src/backend-recovery-integration.test.ts +1575 -0
- package/src/backend-recovery.ts +106 -0
- package/src/backend-routines.ts +375 -0
- package/src/backend-runner.ts +251 -0
- package/src/backend-skills.test.ts +126 -0
- package/src/backend-skills.ts +198 -0
- package/src/backend-stop.test.ts +356 -0
- package/src/backend-subagents.ts +459 -0
- package/src/backend.ts +6035 -0
- package/src/client/FrockBotApp.vue +1026 -0
- package/src/client/SendPayloadView.vue +337 -0
- package/src/client/composer-draft.test.ts +31 -0
- package/src/client/composer-draft.ts +35 -0
- package/src/client/cordis-client-shim.d.ts +15 -0
- package/src/client/index.test.ts +2548 -0
- package/src/client/index.ts +2346 -0
- package/src/client/model-presentation.test.ts +35 -0
- package/src/client/model-presentation.ts +19 -0
- package/src/client/notify.test.ts +89 -0
- package/src/client/notify.ts +101 -0
- package/src/client/skill-invocation.test.ts +143 -0
- package/src/client/skill-invocation.ts +175 -0
- package/src/client/styles.css +1043 -0
- package/src/composition-views.ts +118 -0
- package/src/debug-protocol.test.ts +80 -0
- package/src/debug-protocol.ts +165 -0
- package/src/env.d.ts +10 -0
- package/src/history.test.ts +163 -0
- package/src/history.ts +108 -0
- package/src/host.ts +20 -0
- package/src/index.ts +2 -0
- package/src/manifest.ts +3 -0
- package/src/run-cursor.ts +28 -0
- package/src/run-protocol.test.ts +1281 -0
- package/src/run-protocol.ts +1417 -0
- package/src/settings-links.test.ts +106 -0
- package/src/settings-links.ts +289 -0
- package/src/shared.ts +338 -0
- package/src/skill-protocol.ts +117 -0
- package/src/terminal-records.test.ts +217 -0
- package/src/terminal-records.ts +150 -0
- package/src/unread.test.ts +362 -0
- package/src/unread.ts +675 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +32 -0
- package/README.md +0 -3
package/frockbot.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 4,
|
|
3
|
+
"id": "shell",
|
|
4
|
+
"displayName": "FrockBot Shell",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"compatibility": {
|
|
7
|
+
"frockbot": ">=0.0.1"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"ui-theme": ">=0.0.1"
|
|
11
|
+
},
|
|
12
|
+
"contributions": {
|
|
13
|
+
"backend": [
|
|
14
|
+
{
|
|
15
|
+
"entry": "./backend",
|
|
16
|
+
"host": "bot"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"runtime": {
|
|
20
|
+
"entry": "./agent"
|
|
21
|
+
},
|
|
22
|
+
"client": {
|
|
23
|
+
"entry": "./client",
|
|
24
|
+
"mounts": [
|
|
25
|
+
{
|
|
26
|
+
"slot": "authenticated-root",
|
|
27
|
+
"order": 10000
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"outlets": [
|
|
31
|
+
"frockbot.right-panel",
|
|
32
|
+
"frockbot.computer",
|
|
33
|
+
"frockbot.sidebar-bots",
|
|
34
|
+
"frockbot.sidebar-computer",
|
|
35
|
+
"frockbot.sidebar-actions",
|
|
36
|
+
"frockbot.user-profile",
|
|
37
|
+
"frockbot.bot-actions",
|
|
38
|
+
"frockbot.header-actions",
|
|
39
|
+
"frockbot.overlays",
|
|
40
|
+
"frockbot.bot-identity",
|
|
41
|
+
"frockbot.bot-avatar"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"permissions": ["agent:turn"],
|
|
46
|
+
"configuration": {
|
|
47
|
+
"settings": [],
|
|
48
|
+
"connectionTypes": [],
|
|
49
|
+
"capabilities": [
|
|
50
|
+
{
|
|
51
|
+
"id": "user-voice",
|
|
52
|
+
"kind": "tool",
|
|
53
|
+
"connectionTypes": [],
|
|
54
|
+
"admission": {
|
|
55
|
+
"turnTypes": ["chat"]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "parent-handoff",
|
|
60
|
+
"kind": "tool",
|
|
61
|
+
"connectionTypes": [],
|
|
62
|
+
"admission": {
|
|
63
|
+
"turnTypes": ["automation", "subagent"]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,95 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-shell",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./agent": "./src/agent.ts",
|
|
9
|
+
"./approvals": "./src/approvals.ts",
|
|
10
|
+
"./backend": "./src/backend.ts",
|
|
11
|
+
"./backend-authoring": "./src/backend-authoring.ts",
|
|
12
|
+
"./backend-composition": "./src/backend-composition.ts",
|
|
13
|
+
"./backend-computer": "./src/backend-computer.ts",
|
|
14
|
+
"./backend-contracts": "./src/backend-contracts.ts",
|
|
15
|
+
"./backend-execution": "./src/backend-execution.ts",
|
|
16
|
+
"./backend-image": "./src/backend-image.ts",
|
|
17
|
+
"./backend-isolate": "./src/backend-isolate.ts",
|
|
18
|
+
"./backend-memory": "./src/backend-memory.ts",
|
|
19
|
+
"./backend-recovery": "./src/backend-recovery.ts",
|
|
20
|
+
"./backend-runner": "./src/backend-runner.ts",
|
|
21
|
+
"./backend-skills": "./src/backend-skills.ts",
|
|
22
|
+
"./backend-subagents": "./src/backend-subagents.ts",
|
|
23
|
+
"./client": "./src/client/index.ts",
|
|
24
|
+
"./debug-protocol": "./src/debug-protocol.ts",
|
|
25
|
+
"./client/FrockBotApp.vue": "./src/client/FrockBotApp.vue",
|
|
26
|
+
"./client/notify": "./src/client/notify.ts",
|
|
27
|
+
"./client/styles.css": "./src/client/styles.css",
|
|
28
|
+
"./frockbot.json": "./frockbot.json",
|
|
29
|
+
"./host": "./src/host.ts",
|
|
30
|
+
"./manifest": "./src/manifest.ts",
|
|
31
|
+
"./package.json": "./package.json",
|
|
32
|
+
"./run-cursor": "./src/run-cursor.ts",
|
|
33
|
+
"./run-protocol": "./src/run-protocol.ts",
|
|
34
|
+
"./settings-links": "./src/settings-links.ts",
|
|
35
|
+
"./shared": "./src/shared.ts",
|
|
36
|
+
"./skill-protocol": "./src/skill-protocol.ts",
|
|
37
|
+
"./unread": "./src/unread.ts"
|
|
38
|
+
},
|
|
39
|
+
"frockbot": {
|
|
40
|
+
"manifest": "./frockbot.json"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "vite build",
|
|
44
|
+
"typecheck": "vue-tsc --noEmit -p tsconfig.json"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@cordisjs/plugin-webui": "0.8.2",
|
|
48
|
+
"@frockbot/agent-runtime": "0.1.1",
|
|
49
|
+
"@frockbot/application-foundation": "0.1.1",
|
|
50
|
+
"@frockbot/catalog-core": "0.1.1",
|
|
51
|
+
"@frockbot/client-core": "0.1.1",
|
|
52
|
+
"@frockbot/client-ui": "0.1.1",
|
|
53
|
+
"@frockbot/computer-core": "0.1.1",
|
|
54
|
+
"@frockbot/computer-host-protocol": "0.1.1",
|
|
55
|
+
"@frockbot/configuration-core": "0.1.1",
|
|
56
|
+
"@frockbot/connection-core": "0.1.1",
|
|
57
|
+
"@frockbot/kernel-agent-loop": "0.1.1",
|
|
58
|
+
"@frockbot/kernel-composition": "0.1.1",
|
|
59
|
+
"@frockbot/kernel-contracts": "0.1.1",
|
|
60
|
+
"@frockbot/kernel-do": "0.1.1",
|
|
61
|
+
"@frockbot/machine-protocol": "0.1.1",
|
|
62
|
+
"@frockbot/plugin-authoring": "0.1.1",
|
|
63
|
+
"@frockbot/plugin-bot-template": "0.1.1",
|
|
64
|
+
"@frockbot/plugin-flock": "0.1.1",
|
|
65
|
+
"@frockbot/plugin-image": "0.1.1",
|
|
66
|
+
"@frockbot/plugin-machine-messages": "0.1.1",
|
|
67
|
+
"@frockbot/plugin-mcp": "0.1.1",
|
|
68
|
+
"@frockbot/plugin-memory": "0.1.1",
|
|
69
|
+
"@frockbot/plugin-routines": "0.1.1",
|
|
70
|
+
"@frockbot/plugin-skills": "0.1.1",
|
|
71
|
+
"@frockbot/plugin-subagents": "0.1.1",
|
|
72
|
+
"@frockbot/plugin-user-machine": "0.1.1",
|
|
73
|
+
"@frockbot/protocol": "0.1.1",
|
|
74
|
+
"cordis": "4.0.0-rc.8",
|
|
75
|
+
"vue": "3.5.41"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@cloudflare/workers-types": "latest",
|
|
79
|
+
"@frockbot/plugin-tools": "0.1.1",
|
|
80
|
+
"@types/bun": "1.3.6",
|
|
81
|
+
"@types/node": "26.2.0",
|
|
82
|
+
"@vitejs/plugin-vue": "6.0.8",
|
|
83
|
+
"typescript": "5.9.3",
|
|
84
|
+
"vite": "8.2.2",
|
|
85
|
+
"vue-tsc": "3.3.10"
|
|
86
|
+
},
|
|
87
|
+
"publishConfig": {
|
|
88
|
+
"access": "public"
|
|
89
|
+
},
|
|
6
90
|
"repository": {
|
|
7
91
|
"type": "git",
|
|
8
92
|
"url": "git+https://github.com/timoconnellaus/frockbot.git",
|
|
9
93
|
"directory": "packages/plugin-shell"
|
|
10
|
-
},
|
|
11
|
-
"publishConfig": {
|
|
12
|
-
"access": "public"
|
|
13
94
|
}
|
|
14
95
|
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// The Shell's runtime Contribution: what it admits, what it records, and what
|
|
2
|
+
// ends a Turn.
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import {
|
|
5
|
+
SessionStore,
|
|
6
|
+
type Session,
|
|
7
|
+
type ToolCall,
|
|
8
|
+
type ToolExecutionContext,
|
|
9
|
+
type TurnTypeV1,
|
|
10
|
+
} from "@frockbot/kernel-contracts";
|
|
11
|
+
import { ToolRegistry } from "@frockbot/plugin-tools";
|
|
12
|
+
import { Context } from "cordis";
|
|
13
|
+
import {
|
|
14
|
+
shellAdmissionCeilingV1,
|
|
15
|
+
shellAgentPlugin,
|
|
16
|
+
PARENT_HANDOFF_CAPABILITY_V1,
|
|
17
|
+
SEND_MESSAGE_ALIAS_V1,
|
|
18
|
+
SEND_TO_USER_TOOL_V1,
|
|
19
|
+
USER_VOICE_CAPABILITY_V1,
|
|
20
|
+
WAKE_PARENT_TOOL_V1,
|
|
21
|
+
} from "./agent.ts";
|
|
22
|
+
|
|
23
|
+
const SESSION_ID = "user-1:bot-1";
|
|
24
|
+
|
|
25
|
+
interface Mounted {
|
|
26
|
+
root: Context;
|
|
27
|
+
session: Session;
|
|
28
|
+
dispose(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function mount(): Promise<Mounted> {
|
|
32
|
+
const root = new Context();
|
|
33
|
+
await root.plugin(SessionStore);
|
|
34
|
+
await root.plugin(ToolRegistry);
|
|
35
|
+
const session = root.sessions.create(SESSION_ID);
|
|
36
|
+
session.appendBatch([
|
|
37
|
+
{ type: "turn/start", turn: 4 },
|
|
38
|
+
{ type: "step/start", turn: 4, step: 2 },
|
|
39
|
+
]);
|
|
40
|
+
await root.plugin(shellAgentPlugin);
|
|
41
|
+
return { root, session, dispose: () => root.fiber.dispose() };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function contextFor(turnType: TurnTypeV1): ToolExecutionContext {
|
|
45
|
+
return {
|
|
46
|
+
botId: "bot-1",
|
|
47
|
+
agentId: "bot-1",
|
|
48
|
+
sessionId: SESSION_ID,
|
|
49
|
+
compositionGenerationId: "2026-08-31T00:00:00.000Z:0123456789abcdef",
|
|
50
|
+
turnType,
|
|
51
|
+
effectId: "tool:4:2:0",
|
|
52
|
+
signal: new AbortController().signal,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function call(name: string, input: unknown): ToolCall {
|
|
57
|
+
return { id: "call-1", name, input };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Runs a tool the way the loop does: prepare, then execute what it admits. */
|
|
61
|
+
async function invoke(
|
|
62
|
+
mounted: Mounted,
|
|
63
|
+
turnType: TurnTypeV1,
|
|
64
|
+
toolCall: ToolCall,
|
|
65
|
+
) {
|
|
66
|
+
const context = contextFor(turnType);
|
|
67
|
+
const preparation = await mounted.root.tools.prepare(toolCall, context);
|
|
68
|
+
if (preparation.kind === "denied") return preparation.result;
|
|
69
|
+
return mounted.root.tools.executePrepared(preparation, context);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("the Shell's tool admission", () => {
|
|
73
|
+
test("offers the send tool and its alias on chat turns only", async () => {
|
|
74
|
+
const mounted = await mount();
|
|
75
|
+
try {
|
|
76
|
+
const chat = mounted.root.tools
|
|
77
|
+
.schemas({ turnType: "chat" })
|
|
78
|
+
.map((tool) => tool.name);
|
|
79
|
+
const automation = mounted.root.tools
|
|
80
|
+
.schemas({ turnType: "automation" })
|
|
81
|
+
.map((tool) => tool.name);
|
|
82
|
+
const subagent = mounted.root.tools
|
|
83
|
+
.schemas({ turnType: "subagent" })
|
|
84
|
+
.map((tool) => tool.name);
|
|
85
|
+
|
|
86
|
+
expect(chat).toContain(SEND_TO_USER_TOOL_V1);
|
|
87
|
+
expect(chat).toContain(SEND_MESSAGE_ALIAS_V1);
|
|
88
|
+
expect(chat).not.toContain(WAKE_PARENT_TOOL_V1);
|
|
89
|
+
expect(automation).toEqual([WAKE_PARENT_TOOL_V1]);
|
|
90
|
+
expect(subagent).toEqual([WAKE_PARENT_TOOL_V1]);
|
|
91
|
+
} finally {
|
|
92
|
+
await mounted.dispose();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("no role widens a subagent turn: chat-only tools are absent for all five", async () => {
|
|
97
|
+
const mounted = await mount();
|
|
98
|
+
try {
|
|
99
|
+
for (const subagentRole of [
|
|
100
|
+
"executor",
|
|
101
|
+
"browserUse",
|
|
102
|
+
"computerUse",
|
|
103
|
+
"watchVideo",
|
|
104
|
+
"videoReview",
|
|
105
|
+
]) {
|
|
106
|
+
const names = mounted.root.tools
|
|
107
|
+
.schemas({ turnType: "subagent", subagentRole })
|
|
108
|
+
.map((tool) => tool.name);
|
|
109
|
+
// The turn-type ceiling comes first and no role can reach past it: the
|
|
110
|
+
// Shell's user-facing tools are chat-only, and the hand-off is the only
|
|
111
|
+
// thing any subagent role is offered here.
|
|
112
|
+
expect(names).not.toContain(SEND_TO_USER_TOOL_V1);
|
|
113
|
+
expect(names).not.toContain(SEND_MESSAGE_ALIAS_V1);
|
|
114
|
+
expect(names).toEqual([WAKE_PARENT_TOOL_V1]);
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
await mounted.dispose();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("denies a hallucinated wake_parent on a chat turn", async () => {
|
|
122
|
+
const mounted = await mount();
|
|
123
|
+
try {
|
|
124
|
+
const result = await invoke(
|
|
125
|
+
mounted,
|
|
126
|
+
"chat",
|
|
127
|
+
call(WAKE_PARENT_TOOL_V1, { message: "done" }),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(result).toEqual({
|
|
131
|
+
content: `Tool is not available on a chat turn: ${WAKE_PARENT_TOOL_V1}`,
|
|
132
|
+
isError: true,
|
|
133
|
+
});
|
|
134
|
+
expect(
|
|
135
|
+
mounted.session.events.some((event) => event.type === "wake/parent"),
|
|
136
|
+
).toBe(false);
|
|
137
|
+
} finally {
|
|
138
|
+
await mounted.dispose();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("denies send_to_user and its alias on an automation turn", async () => {
|
|
143
|
+
const mounted = await mount();
|
|
144
|
+
try {
|
|
145
|
+
for (const name of [SEND_TO_USER_TOOL_V1, SEND_MESSAGE_ALIAS_V1]) {
|
|
146
|
+
const result = await invoke(
|
|
147
|
+
mounted,
|
|
148
|
+
"automation",
|
|
149
|
+
call(name, { payload: { type: "text", text: "hi" } }),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(result).toEqual({
|
|
153
|
+
content: `Tool is not available on a automation turn: ${name}`,
|
|
154
|
+
isError: true,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
expect(
|
|
158
|
+
mounted.session.events.some((event) => event.type === "send/to-user"),
|
|
159
|
+
).toBe(false);
|
|
160
|
+
} finally {
|
|
161
|
+
await mounted.dispose();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("bounds each tool by the turn types its manifest Capability declares", () => {
|
|
166
|
+
expect(shellAdmissionCeilingV1(USER_VOICE_CAPABILITY_V1)).toEqual(["chat"]);
|
|
167
|
+
expect(shellAdmissionCeilingV1(PARENT_HANDOFF_CAPABILITY_V1)).toEqual([
|
|
168
|
+
"automation",
|
|
169
|
+
"subagent",
|
|
170
|
+
]);
|
|
171
|
+
expect(shellAdmissionCeilingV1("not-a-capability")).toBeUndefined();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("send_to_user", () => {
|
|
176
|
+
test("records a text send and leaves the Turn running", async () => {
|
|
177
|
+
const mounted = await mount();
|
|
178
|
+
try {
|
|
179
|
+
const result = await invoke(
|
|
180
|
+
mounted,
|
|
181
|
+
"chat",
|
|
182
|
+
call(SEND_TO_USER_TOOL_V1, {
|
|
183
|
+
payload: { type: "text", text: "Booked for Tuesday." },
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(result.isError).toBe(false);
|
|
188
|
+
expect(result.endsTurn).toBeUndefined();
|
|
189
|
+
expect(
|
|
190
|
+
mounted.session.events.find((event) => event.type === "send/to-user"),
|
|
191
|
+
).toMatchObject({
|
|
192
|
+
turn: 4,
|
|
193
|
+
step: 2,
|
|
194
|
+
occurrenceId: "tool:4:2:0",
|
|
195
|
+
payload: { type: "text", text: "Booked for Tuesday." },
|
|
196
|
+
});
|
|
197
|
+
} finally {
|
|
198
|
+
await mounted.dispose();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("ends the Turn on a widget and an approval, and on nothing else", async () => {
|
|
203
|
+
const mounted = await mount();
|
|
204
|
+
try {
|
|
205
|
+
const widget = await invoke(
|
|
206
|
+
mounted,
|
|
207
|
+
"chat",
|
|
208
|
+
call(SEND_TO_USER_TOOL_V1, {
|
|
209
|
+
payload: {
|
|
210
|
+
type: "widget",
|
|
211
|
+
widget: { prompt: "Which day?", options: ["Tue", "Thu"] },
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
const attachment = await invoke(
|
|
216
|
+
mounted,
|
|
217
|
+
"chat",
|
|
218
|
+
call(SEND_TO_USER_TOOL_V1, {
|
|
219
|
+
payload: { type: "attachment", url: "https://files.example/a.pdf" },
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// An approval ends the Turn for the same reason a widget does: the Bot
|
|
224
|
+
// has nothing left to do until a person answers, and the answer is a
|
|
225
|
+
// later Turn's input rather than this one's.
|
|
226
|
+
const approval = await invoke(
|
|
227
|
+
mounted,
|
|
228
|
+
"chat",
|
|
229
|
+
call(SEND_TO_USER_TOOL_V1, {
|
|
230
|
+
payload: {
|
|
231
|
+
type: "approval",
|
|
232
|
+
approvalId: "ap-1",
|
|
233
|
+
action: "Delete the staging database",
|
|
234
|
+
risk: "high",
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
const text = await invoke(
|
|
239
|
+
mounted,
|
|
240
|
+
"chat",
|
|
241
|
+
call(SEND_TO_USER_TOOL_V1, {
|
|
242
|
+
payload: { type: "text", text: "On it." },
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
const secret = await invoke(
|
|
246
|
+
mounted,
|
|
247
|
+
"chat",
|
|
248
|
+
call(SEND_TO_USER_TOOL_V1, {
|
|
249
|
+
payload: {
|
|
250
|
+
type: "secret-request",
|
|
251
|
+
prompt: "Your API key",
|
|
252
|
+
secretName: "api_key",
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
const card = await invoke(
|
|
257
|
+
mounted,
|
|
258
|
+
"chat",
|
|
259
|
+
call(SEND_TO_USER_TOOL_V1, {
|
|
260
|
+
payload: { type: "agent-card", agentId: "bot-2", title: "School" },
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
expect(widget.endsTurn).toBe(true);
|
|
265
|
+
expect(approval.endsTurn).toBe(true);
|
|
266
|
+
expect(attachment.endsTurn).toBeUndefined();
|
|
267
|
+
expect(text.endsTurn).toBeUndefined();
|
|
268
|
+
expect(secret.endsTurn).toBeUndefined();
|
|
269
|
+
expect(card.endsTurn).toBeUndefined();
|
|
270
|
+
expect(
|
|
271
|
+
mounted.session.events
|
|
272
|
+
.filter((event) => event.type === "send/to-user")
|
|
273
|
+
.map((event) =>
|
|
274
|
+
event.type === "send/to-user" ? event.payload.type : undefined,
|
|
275
|
+
),
|
|
276
|
+
).toEqual([
|
|
277
|
+
"widget",
|
|
278
|
+
"attachment",
|
|
279
|
+
"approval",
|
|
280
|
+
"text",
|
|
281
|
+
"secret-request",
|
|
282
|
+
"agent-card",
|
|
283
|
+
]);
|
|
284
|
+
} finally {
|
|
285
|
+
await mounted.dispose();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("refuses a malformed payload without recording anything", async () => {
|
|
290
|
+
const mounted = await mount();
|
|
291
|
+
try {
|
|
292
|
+
const result = await invoke(
|
|
293
|
+
mounted,
|
|
294
|
+
"chat",
|
|
295
|
+
call(SEND_TO_USER_TOOL_V1, { payload: { type: "shout", text: "hi" } }),
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
expect(result.isError).toBe(true);
|
|
299
|
+
expect(result.content).toContain("send_to_user.payload.type is invalid");
|
|
300
|
+
expect(result.endsTurn).toBeUndefined();
|
|
301
|
+
expect(
|
|
302
|
+
mounted.session.events.some((event) => event.type === "send/to-user"),
|
|
303
|
+
).toBe(false);
|
|
304
|
+
} finally {
|
|
305
|
+
await mounted.dispose();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("records the alias under its own name and the same event", async () => {
|
|
310
|
+
const mounted = await mount();
|
|
311
|
+
try {
|
|
312
|
+
const result = await invoke(
|
|
313
|
+
mounted,
|
|
314
|
+
"chat",
|
|
315
|
+
call(SEND_MESSAGE_ALIAS_V1, {
|
|
316
|
+
payload: { type: "text", text: "Legacy." },
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
expect(result.isError).toBe(false);
|
|
321
|
+
expect(
|
|
322
|
+
mounted.session.events.find((event) => event.type === "send/to-user"),
|
|
323
|
+
).toMatchObject({ payload: { type: "text", text: "Legacy." } });
|
|
324
|
+
} finally {
|
|
325
|
+
await mounted.dispose();
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe("wake_parent", () => {
|
|
331
|
+
test("records the hand-off and always ends the Turn", async () => {
|
|
332
|
+
const mounted = await mount();
|
|
333
|
+
try {
|
|
334
|
+
const result = await invoke(
|
|
335
|
+
mounted,
|
|
336
|
+
"automation",
|
|
337
|
+
call(WAKE_PARENT_TOOL_V1, { message: "The invoice is paid." }),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
expect(result).toMatchObject({ isError: false, endsTurn: true });
|
|
341
|
+
expect(
|
|
342
|
+
mounted.session.events.find((event) => event.type === "wake/parent"),
|
|
343
|
+
).toMatchObject({
|
|
344
|
+
turn: 4,
|
|
345
|
+
step: 2,
|
|
346
|
+
occurrenceId: "tool:4:2:0",
|
|
347
|
+
message: "The invoice is paid.",
|
|
348
|
+
});
|
|
349
|
+
} finally {
|
|
350
|
+
await mounted.dispose();
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("refuses an empty hand-off rather than ending the Turn on nothing", async () => {
|
|
355
|
+
const mounted = await mount();
|
|
356
|
+
try {
|
|
357
|
+
const result = await invoke(
|
|
358
|
+
mounted,
|
|
359
|
+
"subagent",
|
|
360
|
+
call(WAKE_PARENT_TOOL_V1, { message: " " }),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
expect(result.isError).toBe(true);
|
|
364
|
+
expect(result.endsTurn).toBeUndefined();
|
|
365
|
+
expect(
|
|
366
|
+
mounted.session.events.some((event) => event.type === "wake/parent"),
|
|
367
|
+
).toBe(false);
|
|
368
|
+
} finally {
|
|
369
|
+
await mounted.dispose();
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
});
|