@kernel.chat/kbot 3.99.30 → 3.99.33
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 +1 -1
- package/dist/agents/security-agent.d.ts +31 -0
- package/dist/agents/security-agent.js +180 -0
- package/dist/agents/security-rules.d.ts +35 -0
- package/dist/agents/security-rules.js +206 -0
- package/dist/agents/specialists.d.ts +6 -0
- package/dist/agents/specialists.js +45 -0
- package/dist/architect.js +5 -0
- package/dist/auth.js +1 -1
- package/dist/channels/matrix.d.ts +4 -0
- package/dist/channels/matrix.js +28 -0
- package/dist/channels/office.d.ts +78 -0
- package/dist/channels/office.js +169 -0
- package/dist/channels/registry.d.ts +8 -0
- package/dist/channels/registry.js +38 -0
- package/dist/channels/signal.d.ts +4 -0
- package/dist/channels/signal.js +29 -0
- package/dist/channels/slack.d.ts +4 -0
- package/dist/channels/slack.js +97 -0
- package/dist/channels/teams.d.ts +4 -0
- package/dist/channels/teams.js +29 -0
- package/dist/channels/telegram.d.ts +4 -0
- package/dist/channels/telegram.js +28 -0
- package/dist/channels/types.d.ts +50 -0
- package/dist/channels/types.js +13 -0
- package/dist/channels/whatsapp.d.ts +4 -0
- package/dist/channels/whatsapp.js +28 -0
- package/dist/computer-use-coordinator.d.ts +44 -0
- package/dist/computer-use-coordinator.js +0 -0
- package/dist/doctor.js +6 -3
- package/dist/file-library.d.ts +76 -0
- package/dist/file-library.js +269 -0
- package/dist/managed-agents-anthropic.d.ts +90 -0
- package/dist/managed-agents-anthropic.js +123 -0
- package/dist/plugins-integrity.d.ts +72 -0
- package/dist/plugins-integrity.js +153 -0
- package/dist/plugins.d.ts +13 -2
- package/dist/plugins.js +87 -10
- package/dist/tools/anthropic-managed-agents-tools.d.ts +22 -0
- package/dist/tools/anthropic-managed-agents-tools.js +191 -0
- package/dist/tools/channel-tools.d.ts +4 -0
- package/dist/tools/channel-tools.js +80 -0
- package/dist/tools/computer-coordinator-tools.d.ts +13 -0
- package/dist/tools/computer-coordinator-tools.js +104 -0
- package/dist/tools/computer.js +463 -299
- package/dist/tools/file-library-tools.d.ts +12 -0
- package/dist/tools/file-library-tools.js +191 -0
- package/dist/tools/image-thoughtful.d.ts +31 -0
- package/dist/tools/image-thoughtful.js +233 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/security-agent-tools.d.ts +34 -0
- package/dist/tools/security-agent-tools.js +30 -0
- package/dist/tools/swarm-2026-04.d.ts +2 -0
- package/dist/tools/swarm-2026-04.js +91 -0
- package/dist/tools/workspace-agent-tools.d.ts +19 -0
- package/dist/tools/workspace-agent-tools.js +191 -0
- package/dist/workspace-agents.d.ts +132 -0
- package/dist/workspace-agents.js +379 -0
- package/package.json +1 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Agent kbot tool definitions.
|
|
3
|
+
*
|
|
4
|
+
* NOT registered in tools/index.ts. Wire up via `registerWorkspaceAgentTools()`
|
|
5
|
+
* from a higher-level bootstrap once the feature is shipped.
|
|
6
|
+
*
|
|
7
|
+
* Each tool returns a string (the kbot tool contract). State persists via
|
|
8
|
+
* the WorkspaceAgent class — file at <root>/<id>.json.
|
|
9
|
+
*/
|
|
10
|
+
import { WorkspaceAgent, } from '../workspace-agents.js';
|
|
11
|
+
function getAgent(opts) {
|
|
12
|
+
return new WorkspaceAgent(opts ?? {});
|
|
13
|
+
}
|
|
14
|
+
function fmt(value) {
|
|
15
|
+
if (typeof value === 'string')
|
|
16
|
+
return value;
|
|
17
|
+
return '```json\n' + JSON.stringify(value, null, 2) + '\n```';
|
|
18
|
+
}
|
|
19
|
+
function parseJsonArray(raw, field) {
|
|
20
|
+
if (raw === undefined || raw === null || raw === '')
|
|
21
|
+
return [];
|
|
22
|
+
if (Array.isArray(raw))
|
|
23
|
+
return raw.map(String);
|
|
24
|
+
if (typeof raw === 'string') {
|
|
25
|
+
const trimmed = raw.trim();
|
|
26
|
+
if (!trimmed)
|
|
27
|
+
return [];
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(trimmed);
|
|
30
|
+
if (!Array.isArray(parsed)) {
|
|
31
|
+
throw new Error(`${field} must be a JSON array`);
|
|
32
|
+
}
|
|
33
|
+
return parsed.map(String);
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
// accept comma-separated as a convenience
|
|
37
|
+
if (!trimmed.startsWith('[')) {
|
|
38
|
+
return trimmed
|
|
39
|
+
.split(',')
|
|
40
|
+
.map(s => s.trim())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
throw e;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`${field} must be a string or array`);
|
|
47
|
+
}
|
|
48
|
+
export const workspaceAgentCreate = {
|
|
49
|
+
name: 'workspace_agent_create',
|
|
50
|
+
description: 'Create a long-running named workspace agent. Persists at ~/.kbot/workspace-agents/<id>.json. ' +
|
|
51
|
+
'Names must be unique per workspace. Permissions enforced via allowedTools whitelist + scopes.',
|
|
52
|
+
parameters: {
|
|
53
|
+
name: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: 'Unique name for this agent in the workspace.',
|
|
56
|
+
required: true,
|
|
57
|
+
},
|
|
58
|
+
mission: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
description: '1–3 sentence mission the agent will pursue across sessions.',
|
|
61
|
+
required: true,
|
|
62
|
+
},
|
|
63
|
+
allowedTools: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'JSON array (or comma-separated list) of tool names this agent may invoke. Empty = no tool calls.',
|
|
66
|
+
},
|
|
67
|
+
scopes: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'JSON array (or comma-separated list) of capability scopes, e.g. ["read:files","write:tools","invoke:slack"].',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
tier: 'free',
|
|
73
|
+
async execute(args) {
|
|
74
|
+
try {
|
|
75
|
+
const allowedTools = parseJsonArray(args.allowedTools, 'allowedTools');
|
|
76
|
+
const scopes = parseJsonArray(args.scopes, 'scopes');
|
|
77
|
+
const result = await getAgent().create({
|
|
78
|
+
name: String(args.name),
|
|
79
|
+
mission: String(args.mission),
|
|
80
|
+
allowedTools,
|
|
81
|
+
scopes,
|
|
82
|
+
});
|
|
83
|
+
return fmt({ created: result });
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
return `Error: ${e.message}`;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
export const workspaceAgentStart = {
|
|
91
|
+
name: 'workspace_agent_start',
|
|
92
|
+
description: 'Start a workspace agent on a task. Transitions status to "running", invokes the planner, persists tool calls into history.',
|
|
93
|
+
parameters: {
|
|
94
|
+
agentId: { type: 'string', description: 'Agent id from create.', required: true },
|
|
95
|
+
taskInput: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
description: 'Task prompt to plan and execute.',
|
|
98
|
+
required: true,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
tier: 'free',
|
|
102
|
+
async execute(args) {
|
|
103
|
+
try {
|
|
104
|
+
const result = await getAgent().start(String(args.agentId), String(args.taskInput));
|
|
105
|
+
return fmt({ started: result });
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return `Error: ${e.message}`;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
export const workspaceAgentResume = {
|
|
113
|
+
name: 'workspace_agent_resume',
|
|
114
|
+
description: 'Resume a paused or failed workspace agent. Loads state from disk and transitions to running.',
|
|
115
|
+
parameters: {
|
|
116
|
+
agentId: { type: 'string', description: 'Agent id.', required: true },
|
|
117
|
+
},
|
|
118
|
+
tier: 'free',
|
|
119
|
+
async execute(args) {
|
|
120
|
+
try {
|
|
121
|
+
const state = await getAgent().resume(String(args.agentId));
|
|
122
|
+
return fmt({
|
|
123
|
+
resumed: { id: state.id, name: state.name, status: state.status },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
return `Error: ${e.message}`;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
export const workspaceAgentStatus = {
|
|
132
|
+
name: 'workspace_agent_status',
|
|
133
|
+
description: 'Get full state for a workspace agent (status, history, plan).',
|
|
134
|
+
parameters: {
|
|
135
|
+
agentId: { type: 'string', description: 'Agent id.', required: true },
|
|
136
|
+
},
|
|
137
|
+
tier: 'free',
|
|
138
|
+
async execute(args) {
|
|
139
|
+
try {
|
|
140
|
+
const state = await getAgent().status(String(args.agentId));
|
|
141
|
+
return fmt(state);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
return `Error: ${e.message}`;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
export const workspaceAgentStop = {
|
|
149
|
+
name: 'workspace_agent_stop',
|
|
150
|
+
description: 'Stop a running workspace agent. Transitions status to "paused".',
|
|
151
|
+
parameters: {
|
|
152
|
+
agentId: { type: 'string', description: 'Agent id.', required: true },
|
|
153
|
+
},
|
|
154
|
+
tier: 'free',
|
|
155
|
+
async execute(args) {
|
|
156
|
+
try {
|
|
157
|
+
const state = await getAgent().stop(String(args.agentId));
|
|
158
|
+
return fmt({
|
|
159
|
+
stopped: { id: state.id, name: state.name, status: state.status },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
return `Error: ${e.message}`;
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
export const workspaceAgentList = {
|
|
168
|
+
name: 'workspace_agent_list',
|
|
169
|
+
description: 'List all workspace agents in this workspace.',
|
|
170
|
+
parameters: {},
|
|
171
|
+
tier: 'free',
|
|
172
|
+
async execute() {
|
|
173
|
+
try {
|
|
174
|
+
const list = await getAgent().list();
|
|
175
|
+
return fmt({ agents: list, count: list.length });
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
return `Error: ${e.message}`;
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
/** Convenience array — caller can iterate to register all six. */
|
|
183
|
+
export const workspaceAgentTools = [
|
|
184
|
+
workspaceAgentCreate,
|
|
185
|
+
workspaceAgentStart,
|
|
186
|
+
workspaceAgentResume,
|
|
187
|
+
workspaceAgentStatus,
|
|
188
|
+
workspaceAgentStop,
|
|
189
|
+
workspaceAgentList,
|
|
190
|
+
];
|
|
191
|
+
//# sourceMappingURL=workspace-agent-tools.js.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Agents — long-running named agents bound to a workspace with
|
|
3
|
+
* permissions and resumable state. Parity with OpenAI's Workspace Agents
|
|
4
|
+
* (Apr 2026). Wraps the hierarchical planner at ./planner/hierarchical/.
|
|
5
|
+
*
|
|
6
|
+
* State JSON shape (one file per agent at <root>/<id>.json):
|
|
7
|
+
* { id, name, mission, allowedTools, scopes, status, createdAt, updatedAt,
|
|
8
|
+
* currentPlanId?, history: [{ ts, event, data }] }
|
|
9
|
+
*
|
|
10
|
+
* Storage root: process.env.KBOT_WORKSPACE_AGENTS_ROOT
|
|
11
|
+
* ?? <homedir>/.kbot/workspace-agents
|
|
12
|
+
*
|
|
13
|
+
* Permissions: every tool invocation must pass through `gate(toolName)` which
|
|
14
|
+
* checks `allowedTools`. Scopes are recorded but enforcement is per-tool via
|
|
15
|
+
* the allowedTools whitelist.
|
|
16
|
+
*/
|
|
17
|
+
import type { AgentOptions } from './agent.js';
|
|
18
|
+
export type WorkspaceAgentStatus = 'idle' | 'running' | 'paused' | 'completed' | 'failed';
|
|
19
|
+
export type Scope = string;
|
|
20
|
+
export interface HistoryEvent {
|
|
21
|
+
ts: string;
|
|
22
|
+
event: string;
|
|
23
|
+
data?: unknown;
|
|
24
|
+
}
|
|
25
|
+
export interface WorkspaceAgentState {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
mission: string;
|
|
29
|
+
allowedTools: string[];
|
|
30
|
+
scopes: Scope[];
|
|
31
|
+
status: WorkspaceAgentStatus;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
updatedAt: string;
|
|
34
|
+
currentPlanId?: string;
|
|
35
|
+
history: HistoryEvent[];
|
|
36
|
+
}
|
|
37
|
+
export interface CreateOptions {
|
|
38
|
+
name: string;
|
|
39
|
+
mission: string;
|
|
40
|
+
allowedTools?: string[];
|
|
41
|
+
scopes?: Scope[];
|
|
42
|
+
}
|
|
43
|
+
export interface CreateResult {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
}
|
|
47
|
+
export interface StartResult {
|
|
48
|
+
id: string;
|
|
49
|
+
status: WorkspaceAgentStatus;
|
|
50
|
+
planId?: string;
|
|
51
|
+
steps: unknown[];
|
|
52
|
+
}
|
|
53
|
+
export interface ListEntry {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
status: WorkspaceAgentStatus;
|
|
57
|
+
}
|
|
58
|
+
export declare class ScopeError extends Error {
|
|
59
|
+
readonly toolName: string;
|
|
60
|
+
constructor(toolName: string, message?: string);
|
|
61
|
+
}
|
|
62
|
+
export declare class WorkspaceAgentError extends Error {
|
|
63
|
+
constructor(message: string);
|
|
64
|
+
}
|
|
65
|
+
export declare function defaultRoot(): string;
|
|
66
|
+
/** A single tool call surfaced by the planner, ready to be gated + recorded. */
|
|
67
|
+
export interface PlannerToolCall {
|
|
68
|
+
tool: string;
|
|
69
|
+
args?: unknown;
|
|
70
|
+
result?: unknown;
|
|
71
|
+
}
|
|
72
|
+
export interface PlannerStartResult {
|
|
73
|
+
planId: string;
|
|
74
|
+
steps: unknown[];
|
|
75
|
+
/**
|
|
76
|
+
* Optional list of tool calls the planner produced. When present, the
|
|
77
|
+
* WorkspaceAgent will run each through `gate()` and `recordToolCall()`,
|
|
78
|
+
* capturing `tool_blocked` events for any that ScopeError out.
|
|
79
|
+
*/
|
|
80
|
+
toolCalls?: PlannerToolCall[];
|
|
81
|
+
/**
|
|
82
|
+
* Free-form notes the planner wants surfaced as history events. Each entry
|
|
83
|
+
* becomes a `planner_note` event on the agent's timeline.
|
|
84
|
+
*/
|
|
85
|
+
notes?: string[];
|
|
86
|
+
}
|
|
87
|
+
export type PlannerStartFn = (taskInput: string, state: WorkspaceAgentState, agentOpts?: AgentOptions | null) => Promise<PlannerStartResult>;
|
|
88
|
+
/**
|
|
89
|
+
* Default planner adapter — implements the 3-tier strategy:
|
|
90
|
+
*
|
|
91
|
+
* Tier 1: KBOT_PLANNER=hierarchical AND non-null agentOpts → real
|
|
92
|
+
* HierarchicalPlanner.planAndExecute. Tool calls extracted from
|
|
93
|
+
* the resulting Action.steps and surfaced for gating.
|
|
94
|
+
* Tier 2: HierarchicalPlanner module loadable but no agentOpts → call
|
|
95
|
+
* createGoal only; emit a TODO note; return early.
|
|
96
|
+
* Tier 3: Module import fails (e.g. test env) → deterministic stub
|
|
97
|
+
* `{ planId: 'stub', steps: [] }`.
|
|
98
|
+
*
|
|
99
|
+
* The function never throws on planner-internal failures: each tier degrades
|
|
100
|
+
* to the next so the WorkspaceAgent.start() lifecycle stays predictable.
|
|
101
|
+
*/
|
|
102
|
+
export declare const defaultPlannerStart: PlannerStartFn;
|
|
103
|
+
export interface WorkspaceAgentOptions {
|
|
104
|
+
/** Override storage root. Defaults to env or ~/.kbot/workspace-agents. */
|
|
105
|
+
root?: string;
|
|
106
|
+
/** Override planner adapter (used by tests). */
|
|
107
|
+
plannerStart?: PlannerStartFn;
|
|
108
|
+
}
|
|
109
|
+
export declare class WorkspaceAgent {
|
|
110
|
+
private readonly root;
|
|
111
|
+
private readonly plannerStart;
|
|
112
|
+
constructor(opts?: WorkspaceAgentOptions);
|
|
113
|
+
create(opts: CreateOptions): Promise<CreateResult>;
|
|
114
|
+
start(agentId: string, taskInput: string, agentOpts?: AgentOptions | null): Promise<StartResult>;
|
|
115
|
+
resume(agentId: string): Promise<WorkspaceAgentState>;
|
|
116
|
+
stop(agentId: string): Promise<WorkspaceAgentState>;
|
|
117
|
+
status(agentId: string): Promise<WorkspaceAgentState>;
|
|
118
|
+
list(): Promise<ListEntry[]>;
|
|
119
|
+
/**
|
|
120
|
+
* Permission gate. Throws ScopeError if the tool isn't in allowedTools and
|
|
121
|
+
* appends a `tool_denied` event to history. On allow, appends `tool_allowed`.
|
|
122
|
+
*/
|
|
123
|
+
gate(agentId: string, toolName: string): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Append a tool-call record to the agent's history. Caller must call
|
|
126
|
+
* `gate()` first; this method does NOT enforce permissions.
|
|
127
|
+
*/
|
|
128
|
+
recordToolCall(agentId: string, toolName: string, args: unknown, result?: unknown): Promise<void>;
|
|
129
|
+
private appendEvent;
|
|
130
|
+
private requireState;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=workspace-agents.d.ts.map
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Agents — long-running named agents bound to a workspace with
|
|
3
|
+
* permissions and resumable state. Parity with OpenAI's Workspace Agents
|
|
4
|
+
* (Apr 2026). Wraps the hierarchical planner at ./planner/hierarchical/.
|
|
5
|
+
*
|
|
6
|
+
* State JSON shape (one file per agent at <root>/<id>.json):
|
|
7
|
+
* { id, name, mission, allowedTools, scopes, status, createdAt, updatedAt,
|
|
8
|
+
* currentPlanId?, history: [{ ts, event, data }] }
|
|
9
|
+
*
|
|
10
|
+
* Storage root: process.env.KBOT_WORKSPACE_AGENTS_ROOT
|
|
11
|
+
* ?? <homedir>/.kbot/workspace-agents
|
|
12
|
+
*
|
|
13
|
+
* Permissions: every tool invocation must pass through `gate(toolName)` which
|
|
14
|
+
* checks `allowedTools`. Scopes are recorded but enforcement is per-tool via
|
|
15
|
+
* the allowedTools whitelist.
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import { promises as fs } from 'node:fs';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
export class ScopeError extends Error {
|
|
22
|
+
toolName;
|
|
23
|
+
constructor(toolName, message) {
|
|
24
|
+
super(message ?? `Tool "${toolName}" is not in this agent's allowedTools`);
|
|
25
|
+
this.name = 'ScopeError';
|
|
26
|
+
this.toolName = toolName;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export class WorkspaceAgentError extends Error {
|
|
30
|
+
constructor(message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'WorkspaceAgentError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
// Storage helpers
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
export function defaultRoot() {
|
|
39
|
+
const env = process.env.KBOT_WORKSPACE_AGENTS_ROOT;
|
|
40
|
+
if (env && env.trim().length > 0)
|
|
41
|
+
return env;
|
|
42
|
+
return path.join(os.homedir(), '.kbot', 'workspace-agents');
|
|
43
|
+
}
|
|
44
|
+
async function ensureDir(dir) {
|
|
45
|
+
await fs.mkdir(dir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
function statePath(root, id) {
|
|
48
|
+
return path.join(root, `${id}.json`);
|
|
49
|
+
}
|
|
50
|
+
async function readState(root, id) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = await fs.readFile(statePath(root, id), 'utf8');
|
|
53
|
+
return JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
if (e.code === 'ENOENT')
|
|
57
|
+
return null;
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function writeState(root, state) {
|
|
62
|
+
await ensureDir(root);
|
|
63
|
+
const tmp = statePath(root, state.id) + '.tmp';
|
|
64
|
+
await fs.writeFile(tmp, JSON.stringify(state, null, 2), 'utf8');
|
|
65
|
+
await fs.rename(tmp, statePath(root, state.id));
|
|
66
|
+
}
|
|
67
|
+
async function listAll(root) {
|
|
68
|
+
try {
|
|
69
|
+
const entries = await fs.readdir(root);
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const e of entries) {
|
|
72
|
+
if (!e.endsWith('.json'))
|
|
73
|
+
continue;
|
|
74
|
+
try {
|
|
75
|
+
const raw = await fs.readFile(path.join(root, e), 'utf8');
|
|
76
|
+
out.push(JSON.parse(raw));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// skip corrupt entries
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
if (e.code === 'ENOENT')
|
|
86
|
+
return [];
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Default planner adapter — implements the 3-tier strategy:
|
|
92
|
+
*
|
|
93
|
+
* Tier 1: KBOT_PLANNER=hierarchical AND non-null agentOpts → real
|
|
94
|
+
* HierarchicalPlanner.planAndExecute. Tool calls extracted from
|
|
95
|
+
* the resulting Action.steps and surfaced for gating.
|
|
96
|
+
* Tier 2: HierarchicalPlanner module loadable but no agentOpts → call
|
|
97
|
+
* createGoal only; emit a TODO note; return early.
|
|
98
|
+
* Tier 3: Module import fails (e.g. test env) → deterministic stub
|
|
99
|
+
* `{ planId: 'stub', steps: [] }`.
|
|
100
|
+
*
|
|
101
|
+
* The function never throws on planner-internal failures: each tier degrades
|
|
102
|
+
* to the next so the WorkspaceAgent.start() lifecycle stays predictable.
|
|
103
|
+
*/
|
|
104
|
+
export const defaultPlannerStart = async (taskInput, state, agentOpts) => {
|
|
105
|
+
// Try to import the planner module first. If this fails we're in Tier 3.
|
|
106
|
+
let mod;
|
|
107
|
+
try {
|
|
108
|
+
mod = await import('./planner/hierarchical/session-planner.js');
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Tier 3: stub fallback (current behavior).
|
|
112
|
+
return { planId: 'stub', steps: [] };
|
|
113
|
+
}
|
|
114
|
+
const planner = new mod.HierarchicalPlanner();
|
|
115
|
+
// Tier 1: real planner — needs both the env flag and agentOpts.
|
|
116
|
+
if (process.env.KBOT_PLANNER === 'hierarchical' && agentOpts) {
|
|
117
|
+
try {
|
|
118
|
+
const goal = await planner.createGoal({
|
|
119
|
+
title: state.name,
|
|
120
|
+
intent: state.mission,
|
|
121
|
+
acceptance: [taskInput],
|
|
122
|
+
tags: ['workspace-agent', state.id],
|
|
123
|
+
});
|
|
124
|
+
const result = await planner.planAndExecute(taskInput, {
|
|
125
|
+
sessionId: state.id,
|
|
126
|
+
agentOpts,
|
|
127
|
+
autoApprove: true,
|
|
128
|
+
});
|
|
129
|
+
const steps = result.action.steps;
|
|
130
|
+
const toolCalls = steps
|
|
131
|
+
.filter(s => typeof s.tool === 'string' && s.tool.length > 0)
|
|
132
|
+
.map(s => ({
|
|
133
|
+
tool: s.tool,
|
|
134
|
+
args: s.args,
|
|
135
|
+
result: s.result,
|
|
136
|
+
}));
|
|
137
|
+
return {
|
|
138
|
+
planId: goal.id,
|
|
139
|
+
steps,
|
|
140
|
+
toolCalls,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
// If tier-1 itself throws, don't crash the whole start() — degrade to
|
|
145
|
+
// Tier 2 so we still record the goal and a planner_note explaining why.
|
|
146
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
147
|
+
try {
|
|
148
|
+
const goal = await planner.createGoal({
|
|
149
|
+
title: state.name,
|
|
150
|
+
intent: state.mission,
|
|
151
|
+
acceptance: [taskInput],
|
|
152
|
+
tags: ['workspace-agent', state.id],
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
planId: goal.id,
|
|
156
|
+
steps: [],
|
|
157
|
+
notes: [
|
|
158
|
+
`tier-1 planner failed (${msg}); recorded goal only`,
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return { planId: 'stub', steps: [] };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Tier 2: planner loadable but no agentOpts (or flag absent) — record a
|
|
168
|
+
// goal and surface a TODO so callers know to wire AgentOptions through.
|
|
169
|
+
try {
|
|
170
|
+
const goal = await planner.createGoal({
|
|
171
|
+
title: state.name,
|
|
172
|
+
intent: state.mission,
|
|
173
|
+
acceptance: [taskInput],
|
|
174
|
+
tags: ['workspace-agent', state.id],
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
planId: goal.id,
|
|
178
|
+
steps: [],
|
|
179
|
+
notes: [
|
|
180
|
+
'real planAndExecute requires AgentOptions; configure caller to pass them.',
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Tier 3: createGoal failed (e.g. read-only home dir) → stub.
|
|
186
|
+
return { planId: 'stub', steps: [] };
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
export class WorkspaceAgent {
|
|
190
|
+
root;
|
|
191
|
+
plannerStart;
|
|
192
|
+
constructor(opts = {}) {
|
|
193
|
+
this.root = opts.root ?? defaultRoot();
|
|
194
|
+
this.plannerStart = opts.plannerStart ?? defaultPlannerStart;
|
|
195
|
+
}
|
|
196
|
+
// ── Public API ───────────────────────────────────────────────────────────
|
|
197
|
+
async create(opts) {
|
|
198
|
+
if (!opts.name || !opts.name.trim()) {
|
|
199
|
+
throw new WorkspaceAgentError('create: name is required');
|
|
200
|
+
}
|
|
201
|
+
if (!opts.mission || !opts.mission.trim()) {
|
|
202
|
+
throw new WorkspaceAgentError('create: mission is required');
|
|
203
|
+
}
|
|
204
|
+
const existing = await listAll(this.root);
|
|
205
|
+
if (existing.some(s => s.name === opts.name)) {
|
|
206
|
+
throw new WorkspaceAgentError(`create: an agent named "${opts.name}" already exists in this workspace`);
|
|
207
|
+
}
|
|
208
|
+
const now = new Date().toISOString();
|
|
209
|
+
const state = {
|
|
210
|
+
id: randomUUID(),
|
|
211
|
+
name: opts.name,
|
|
212
|
+
mission: opts.mission,
|
|
213
|
+
allowedTools: opts.allowedTools ?? [],
|
|
214
|
+
scopes: opts.scopes ?? [],
|
|
215
|
+
status: 'idle',
|
|
216
|
+
createdAt: now,
|
|
217
|
+
updatedAt: now,
|
|
218
|
+
history: [
|
|
219
|
+
{ ts: now, event: 'created', data: { name: opts.name } },
|
|
220
|
+
],
|
|
221
|
+
};
|
|
222
|
+
await writeState(this.root, state);
|
|
223
|
+
return { id: state.id, name: state.name };
|
|
224
|
+
}
|
|
225
|
+
async start(agentId, taskInput, agentOpts = null) {
|
|
226
|
+
const state = await this.requireState(agentId);
|
|
227
|
+
if (state.status === 'running') {
|
|
228
|
+
throw new WorkspaceAgentError(`start: agent ${agentId} is already running`);
|
|
229
|
+
}
|
|
230
|
+
if (state.status === 'completed') {
|
|
231
|
+
throw new WorkspaceAgentError(`start: agent ${agentId} is completed; create a new agent`);
|
|
232
|
+
}
|
|
233
|
+
state.status = 'running';
|
|
234
|
+
state.updatedAt = new Date().toISOString();
|
|
235
|
+
state.history.push({
|
|
236
|
+
ts: state.updatedAt,
|
|
237
|
+
event: 'started',
|
|
238
|
+
data: { taskInput },
|
|
239
|
+
});
|
|
240
|
+
await writeState(this.root, state);
|
|
241
|
+
let planResult;
|
|
242
|
+
try {
|
|
243
|
+
planResult = await this.plannerStart(taskInput, state, agentOpts);
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
247
|
+
state.status = 'failed';
|
|
248
|
+
state.updatedAt = new Date().toISOString();
|
|
249
|
+
state.history.push({
|
|
250
|
+
ts: state.updatedAt,
|
|
251
|
+
event: 'planner_error',
|
|
252
|
+
data: { message: msg },
|
|
253
|
+
});
|
|
254
|
+
await writeState(this.root, state);
|
|
255
|
+
throw new WorkspaceAgentError(`start: planner failed: ${msg}`);
|
|
256
|
+
}
|
|
257
|
+
state.currentPlanId = planResult.planId;
|
|
258
|
+
state.updatedAt = new Date().toISOString();
|
|
259
|
+
state.history.push({
|
|
260
|
+
ts: state.updatedAt,
|
|
261
|
+
event: 'plan_created',
|
|
262
|
+
data: { planId: planResult.planId, stepCount: planResult.steps.length },
|
|
263
|
+
});
|
|
264
|
+
await writeState(this.root, state);
|
|
265
|
+
// Surface any planner-emitted notes onto the agent's timeline.
|
|
266
|
+
if (planResult.notes && planResult.notes.length > 0) {
|
|
267
|
+
for (const note of planResult.notes) {
|
|
268
|
+
await this.appendEvent(agentId, 'planner_note', { message: note });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Gate + record every tool call the planner produced. ScopeError from
|
|
272
|
+
// gate() must NOT escape start() — convert to a `tool_blocked` event and
|
|
273
|
+
// continue with the rest.
|
|
274
|
+
if (planResult.toolCalls && planResult.toolCalls.length > 0) {
|
|
275
|
+
for (const call of planResult.toolCalls) {
|
|
276
|
+
try {
|
|
277
|
+
await this.gate(agentId, call.tool);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
if (err instanceof ScopeError) {
|
|
281
|
+
await this.appendEvent(agentId, 'tool_blocked', {
|
|
282
|
+
tool: call.tool,
|
|
283
|
+
reason: err.message,
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
await this.recordToolCall(agentId, call.tool, call.args, call.result);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
id: state.id,
|
|
294
|
+
status: state.status,
|
|
295
|
+
planId: planResult.planId,
|
|
296
|
+
steps: planResult.steps,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async resume(agentId) {
|
|
300
|
+
const state = await this.requireState(agentId);
|
|
301
|
+
if (state.status !== 'paused' && state.status !== 'failed') {
|
|
302
|
+
throw new WorkspaceAgentError(`resume: agent ${agentId} has status "${state.status}"; resume only allowed from paused or failed`);
|
|
303
|
+
}
|
|
304
|
+
state.status = 'running';
|
|
305
|
+
state.updatedAt = new Date().toISOString();
|
|
306
|
+
state.history.push({ ts: state.updatedAt, event: 'resumed' });
|
|
307
|
+
await writeState(this.root, state);
|
|
308
|
+
return state;
|
|
309
|
+
}
|
|
310
|
+
async stop(agentId) {
|
|
311
|
+
const state = await this.requireState(agentId);
|
|
312
|
+
state.status = 'paused';
|
|
313
|
+
state.updatedAt = new Date().toISOString();
|
|
314
|
+
state.history.push({ ts: state.updatedAt, event: 'stopped' });
|
|
315
|
+
await writeState(this.root, state);
|
|
316
|
+
return state;
|
|
317
|
+
}
|
|
318
|
+
async status(agentId) {
|
|
319
|
+
return this.requireState(agentId);
|
|
320
|
+
}
|
|
321
|
+
async list() {
|
|
322
|
+
const all = await listAll(this.root);
|
|
323
|
+
return all.map(s => ({ id: s.id, name: s.name, status: s.status }));
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Permission gate. Throws ScopeError if the tool isn't in allowedTools and
|
|
327
|
+
* appends a `tool_denied` event to history. On allow, appends `tool_allowed`.
|
|
328
|
+
*/
|
|
329
|
+
async gate(agentId, toolName) {
|
|
330
|
+
const state = await this.requireState(agentId);
|
|
331
|
+
if (!state.allowedTools.includes(toolName)) {
|
|
332
|
+
state.history.push({
|
|
333
|
+
ts: new Date().toISOString(),
|
|
334
|
+
event: 'tool_denied',
|
|
335
|
+
data: { tool: toolName },
|
|
336
|
+
});
|
|
337
|
+
state.updatedAt = new Date().toISOString();
|
|
338
|
+
await writeState(this.root, state);
|
|
339
|
+
throw new ScopeError(toolName);
|
|
340
|
+
}
|
|
341
|
+
state.history.push({
|
|
342
|
+
ts: new Date().toISOString(),
|
|
343
|
+
event: 'tool_allowed',
|
|
344
|
+
data: { tool: toolName },
|
|
345
|
+
});
|
|
346
|
+
state.updatedAt = new Date().toISOString();
|
|
347
|
+
await writeState(this.root, state);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Append a tool-call record to the agent's history. Caller must call
|
|
351
|
+
* `gate()` first; this method does NOT enforce permissions.
|
|
352
|
+
*/
|
|
353
|
+
async recordToolCall(agentId, toolName, args, result) {
|
|
354
|
+
const state = await this.requireState(agentId);
|
|
355
|
+
state.history.push({
|
|
356
|
+
ts: new Date().toISOString(),
|
|
357
|
+
event: 'tool_call',
|
|
358
|
+
data: { tool: toolName, args, result },
|
|
359
|
+
});
|
|
360
|
+
state.updatedAt = new Date().toISOString();
|
|
361
|
+
await writeState(this.root, state);
|
|
362
|
+
}
|
|
363
|
+
// ── Internals ────────────────────────────────────────────────────────────
|
|
364
|
+
async appendEvent(agentId, event, data) {
|
|
365
|
+
const state = await this.requireState(agentId);
|
|
366
|
+
const ts = new Date().toISOString();
|
|
367
|
+
state.history.push({ ts, event, data });
|
|
368
|
+
state.updatedAt = ts;
|
|
369
|
+
await writeState(this.root, state);
|
|
370
|
+
}
|
|
371
|
+
async requireState(agentId) {
|
|
372
|
+
const state = await readState(this.root, agentId);
|
|
373
|
+
if (!state) {
|
|
374
|
+
throw new WorkspaceAgentError(`agent ${agentId} not found`);
|
|
375
|
+
}
|
|
376
|
+
return state;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
//# sourceMappingURL=workspace-agents.js.map
|