@capekai/core 1.0.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/README.md +12 -0
- package/package.json +105 -0
- package/src/adapters/ai-sdk.ts +84 -0
- package/src/compaction/contracts.ts +82 -0
- package/src/compaction/executor.ts +161 -0
- package/src/compaction/policy.ts +318 -0
- package/src/compaction/recovery.ts +139 -0
- package/src/compaction/task.ts +540 -0
- package/src/configuration/contracts.ts +58 -0
- package/src/configuration/defaults.ts +27 -0
- package/src/configuration/runtime.ts +42 -0
- package/src/configuration/single-model.ts +75 -0
- package/src/context/assembler.ts +112 -0
- package/src/context/index.ts +2 -0
- package/src/context/sources.ts +119 -0
- package/src/context/workspace.ts +63 -0
- package/src/core/agent.ts +401 -0
- package/src/core/build-tools.ts +139 -0
- package/src/core/chat-handler.ts +858 -0
- package/src/core/error-handling.ts +18 -0
- package/src/core/fork.ts +103 -0
- package/src/core/interrupt.ts +192 -0
- package/src/core/message-utils.ts +261 -0
- package/src/core/model-utils.ts +149 -0
- package/src/core/part-utils.ts +88 -0
- package/src/core/provider-utils.ts +67 -0
- package/src/core/revert.ts +46 -0
- package/src/core/step-handlers.ts +157 -0
- package/src/core/stream/finalization.ts +65 -0
- package/src/core/stream/stream-config.ts +82 -0
- package/src/core/stream-handlers.ts +242 -0
- package/src/core/structured-output.ts +68 -0
- package/src/core/tool-builders/agent-tools.ts +71 -0
- package/src/core/tool-builders/external-tools.ts +179 -0
- package/src/core/tool-builders/types.ts +16 -0
- package/src/core/tool-builders/workspace-tools.ts +293 -0
- package/src/core/tool-capabilities.ts +65 -0
- package/src/goals/evaluator.ts +171 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/loop.ts +167 -0
- package/src/goals/service.ts +39 -0
- package/src/index.ts +10 -0
- package/src/internal/ask-authority.ts +29 -0
- package/src/internal/composition.ts +44 -0
- package/src/internal/configuration.ts +22 -0
- package/src/internal/execution.ts +108 -0
- package/src/internal/hosts.ts +64 -0
- package/src/internal/plugins.ts +71 -0
- package/src/internal/providers.ts +32 -0
- package/src/internal/sandbox.ts +19 -0
- package/src/internal/tools.ts +48 -0
- package/src/internal/workspace.ts +25 -0
- package/src/kernel/diagnostics.ts +249 -0
- package/src/kernel/errors.ts +120 -0
- package/src/kernel/events.ts +82 -0
- package/src/kernel/index.ts +72 -0
- package/src/kernel/kernel.ts +62 -0
- package/src/kernel/lifecycle.ts +72 -0
- package/src/kernel/plugin.ts +218 -0
- package/src/kernel/registry.ts +493 -0
- package/src/kernel/scope.ts +776 -0
- package/src/kernel/service-key.ts +19 -0
- package/src/kernel/types.ts +317 -0
- package/src/memory/index.ts +2 -0
- package/src/memory/memory-tool.ts +75 -0
- package/src/memory/registry.ts +172 -0
- package/src/permission/ask-user-api.ts +70 -0
- package/src/permission/contracts.ts +135 -0
- package/src/permission/permission-request-manager.ts +58 -0
- package/src/permission/policy.ts +277 -0
- package/src/permission/runtime.ts +612 -0
- package/src/plugins/compaction-policy.ts +46 -0
- package/src/plugins/compose.ts +171 -0
- package/src/plugins/context-sections.ts +246 -0
- package/src/plugins/default-agent-driver.ts +14 -0
- package/src/plugins/facade-plugins.ts +129 -0
- package/src/plugins/goal-domain.ts +82 -0
- package/src/plugins/legacy-system-message.ts +152 -0
- package/src/plugins/loaded-tools.ts +23 -0
- package/src/plugins/memory-domain.ts +264 -0
- package/src/plugins/orchestrator-session.ts +29 -0
- package/src/plugins/permission-policy.ts +49 -0
- package/src/plugins/retry-policy.ts +28 -0
- package/src/plugins/scheduler-domain.ts +192 -0
- package/src/plugins/service-keys.ts +294 -0
- package/src/plugins/session-search-domain.ts +238 -0
- package/src/plugins/skills-domain.ts +272 -0
- package/src/plugins/subagent-domain.ts +287 -0
- package/src/plugins/tool-catalog.ts +78 -0
- package/src/plugins/tool-output-policy.ts +52 -0
- package/src/plugins/value-plugins.ts +150 -0
- package/src/plugins/workflow-domain.ts +198 -0
- package/src/plugins/workspace-policy.ts +37 -0
- package/src/providers/registry.ts +63 -0
- package/src/providers/types.ts +44 -0
- package/src/retry/policy.ts +282 -0
- package/src/retry/stream-chat.ts +312 -0
- package/src/runtime/agent-runtime.ts +83 -0
- package/src/runtime/default-agent-driver.ts +23 -0
- package/src/runtime/domain-tool-source.ts +156 -0
- package/src/runtime/events.ts +61 -0
- package/src/runtime/host-dependencies.ts +71 -0
- package/src/runtime/host-guidance.ts +22 -0
- package/src/runtime/host-layout.ts +23 -0
- package/src/runtime/host.ts +129 -0
- package/src/runtime/standalone-host.ts +118 -0
- package/src/sandbox/controller.ts +204 -0
- package/src/sandbox/model.ts +305 -0
- package/src/sandbox/provider.ts +53 -0
- package/src/sandbox/types.ts +110 -0
- package/src/scheduler/host.ts +22 -0
- package/src/scheduler/scheduler-tool.ts +172 -0
- package/src/session-search/host.ts +56 -0
- package/src/session-search/index.ts +23 -0
- package/src/session-search/session-search-tool.ts +151 -0
- package/src/skills/index.ts +3 -0
- package/src/skills/registry.ts +63 -0
- package/src/skills/skill-manage-tool.ts +205 -0
- package/src/skills/skill-tool.ts +42 -0
- package/src/storage/contracts.ts +159 -0
- package/src/storage/memory.ts +321 -0
- package/src/storage/options.ts +75 -0
- package/src/storage/runtime.ts +115 -0
- package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
- package/src/storage/sqlite.ts +321 -0
- package/src/storage/tool-output-artifacts.ts +75 -0
- package/src/storage.ts +31 -0
- package/src/subagent/child-session.ts +282 -0
- package/src/subagent/guidance.ts +8 -0
- package/src/subagent/policy.ts +198 -0
- package/src/subagent/task-tool.ts +584 -0
- package/src/tool-output/contracts.ts +111 -0
- package/src/tool-output/policy.ts +410 -0
- package/src/tool.ts +1 -0
- package/src/tools/executor.ts +258 -0
- package/src/tools/install-manifest.ts +40 -0
- package/src/tools/llm-api.ts +77 -0
- package/src/tools/registry.ts +206 -0
- package/src/tools/tool-artifact.ts +182 -0
- package/src/tools/tool-source.ts +53 -0
- package/src/utils/errors.ts +334 -0
- package/src/utils/strip-visualization.ts +50 -0
- package/src/workflow/decomposer.ts +139 -0
- package/src/workflow/execution.ts +523 -0
- package/src/workflow/orchestrator-session.ts +161 -0
- package/src/workflow/synthesizer.ts +130 -0
- package/src/workspace/contracts.ts +135 -0
- package/src/workspace/policy.ts +327 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { tool, jsonSchema } from 'ai';
|
|
2
|
+
import {
|
|
3
|
+
getContributedDomainToolPayloads,
|
|
4
|
+
getDomainToolFallback,
|
|
5
|
+
mergeDomainToolVisualization,
|
|
6
|
+
type DomainToolPayload,
|
|
7
|
+
} from '../../runtime/domain-tool-source';
|
|
8
|
+
import { getWorkspace } from '../../storage/runtime';
|
|
9
|
+
import {
|
|
10
|
+
createAskApi,
|
|
11
|
+
rejectPendingAsksByToolCallId,
|
|
12
|
+
type AskBroadcastFn,
|
|
13
|
+
} from '../../permission/ask-user-api';
|
|
14
|
+
import { interruptManager } from '../interrupt';
|
|
15
|
+
import type { WorkflowInput } from '@capekai/types';
|
|
16
|
+
import type { PermissionRiskLevel } from '@capekai/tool';
|
|
17
|
+
import type { ToolMap } from './types';
|
|
18
|
+
|
|
19
|
+
export interface WorkspaceToolsOptions {
|
|
20
|
+
workspaceId: string;
|
|
21
|
+
workspacePath: string;
|
|
22
|
+
rootSessionId: string;
|
|
23
|
+
sessionId: string;
|
|
24
|
+
canSpawn: boolean;
|
|
25
|
+
canSpawnSubagents?: boolean | string[] | null;
|
|
26
|
+
allowSelfAsSubagent?: boolean;
|
|
27
|
+
allowedSubagentIds?: string[];
|
|
28
|
+
broadcastFn?: AskBroadcastFn;
|
|
29
|
+
agentId?: string | null;
|
|
30
|
+
allowedSkills?: string[] | null;
|
|
31
|
+
agentSkillsDir?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function buildWorkspaceTools(options: WorkspaceToolsOptions): Promise<ToolMap> {
|
|
35
|
+
const {
|
|
36
|
+
workspaceId,
|
|
37
|
+
workspacePath,
|
|
38
|
+
rootSessionId,
|
|
39
|
+
sessionId,
|
|
40
|
+
canSpawn,
|
|
41
|
+
canSpawnSubagents,
|
|
42
|
+
allowSelfAsSubagent,
|
|
43
|
+
broadcastFn,
|
|
44
|
+
agentId,
|
|
45
|
+
allowedSkills,
|
|
46
|
+
agentSkillsDir,
|
|
47
|
+
} = options;
|
|
48
|
+
|
|
49
|
+
const tools: ToolMap = {};
|
|
50
|
+
const workspace = await getWorkspace(workspaceId);
|
|
51
|
+
if (!workspace) return tools;
|
|
52
|
+
|
|
53
|
+
// The generic contributed-domain-tool payload map is resolved once and
|
|
54
|
+
// shared by the skill, memory, workflow, skill_manage, session_search,
|
|
55
|
+
// and scheduler builders: null means the unscoped path (registered
|
|
56
|
+
// fallbacks may apply), an empty map means a composed scope without
|
|
57
|
+
// domain payloads (fallbacks disabled).
|
|
58
|
+
const scopedDomainPayloads = getContributedDomainToolPayloads();
|
|
59
|
+
const domainPayload = (name: string): DomainToolPayload | null =>
|
|
60
|
+
scopedDomainPayloads === null
|
|
61
|
+
? getDomainToolFallback(name)
|
|
62
|
+
: scopedDomainPayloads.get(name) ?? null;
|
|
63
|
+
|
|
64
|
+
// ── Skill tool ────────────────────────────────────────────
|
|
65
|
+
if (workspacePath) {
|
|
66
|
+
const skillPayload = domainPayload('skill');
|
|
67
|
+
const skillDefinition = await skillPayload?.resolveDefinition?.(sessionId, {
|
|
68
|
+
workspacePath,
|
|
69
|
+
allowedSkills,
|
|
70
|
+
agentSkillsDir,
|
|
71
|
+
});
|
|
72
|
+
if (skillPayload && skillDefinition) {
|
|
73
|
+
tools['skill'] = tool({
|
|
74
|
+
description: skillDefinition.description,
|
|
75
|
+
inputSchema: jsonSchema(skillDefinition.inputSchema),
|
|
76
|
+
execute: async (args: Record<string, unknown>) =>
|
|
77
|
+
skillPayload.execute(args, {
|
|
78
|
+
workspaceId,
|
|
79
|
+
sessionId,
|
|
80
|
+
ask: async () => {
|
|
81
|
+
throw new Error('Cannot ask user: no broadcast channel available');
|
|
82
|
+
},
|
|
83
|
+
agentId,
|
|
84
|
+
workspacePath,
|
|
85
|
+
allowedSkills,
|
|
86
|
+
agentSkillsDir,
|
|
87
|
+
}).then((result) => mergeDomainToolVisualization(skillPayload, args, result)),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Memory tool ───────────────────────────────────────────
|
|
93
|
+
const memoryPayload = domainPayload('memory');
|
|
94
|
+
const memorySettings = workspace.settings?.memory;
|
|
95
|
+
if (memoryPayload && memorySettings?.enabled) {
|
|
96
|
+
const permissionRisk = memorySettings.permissionRisk;
|
|
97
|
+
tools['memory'] = tool({
|
|
98
|
+
description: memoryPayload.description,
|
|
99
|
+
inputSchema: jsonSchema(memoryPayload.inputSchema),
|
|
100
|
+
execute: async (args: Record<string, unknown>, { toolCallId }: { toolCallId: string }) => {
|
|
101
|
+
const _toolAbortController = interruptManager.registerToolExecution(sessionId, toolCallId);
|
|
102
|
+
try {
|
|
103
|
+
const askApi = createAskApiOrThrow(sessionId, toolCallId, 'memory', broadcastFn, workspaceId, rootSessionId);
|
|
104
|
+
const result = await memoryPayload.execute(args, {
|
|
105
|
+
workspaceId,
|
|
106
|
+
sessionId,
|
|
107
|
+
ask: (ask: import('@capekai/tool').Ask) => askApi(ask),
|
|
108
|
+
agentId,
|
|
109
|
+
workspacePath,
|
|
110
|
+
permissionRisk,
|
|
111
|
+
});
|
|
112
|
+
return mergeDomainToolVisualization(memoryPayload, args, result);
|
|
113
|
+
} finally {
|
|
114
|
+
interruptManager.unregisterToolExecution(sessionId, toolCallId);
|
|
115
|
+
await rejectPendingAsksByToolCallId(toolCallId);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Workflow tool ─────────────────────────────────────────────
|
|
122
|
+
// Same generic domain seam as session_search and scheduler: the domain
|
|
123
|
+
// payload owns the depth gate (isEnabled) and the dynamic definition
|
|
124
|
+
// (allowed leaf agent list); the workspace settings gate stays here, and
|
|
125
|
+
// the allowed-subagent list is captured at build time from the resolved
|
|
126
|
+
// definition exactly like pre-C5.
|
|
127
|
+
const workflowPayload = domainPayload('workflow');
|
|
128
|
+
const workflowSettings = workspace.settings?.workflow;
|
|
129
|
+
if (workflowPayload && workflowSettings?.enabled && canSpawn && await workflowPayload.isEnabled?.(workspaceId, sessionId) === true) {
|
|
130
|
+
const workflowDefinition = await workflowPayload.resolveDefinition?.(sessionId, {
|
|
131
|
+
canSpawnSubagents,
|
|
132
|
+
allowSelfAsSubagent,
|
|
133
|
+
});
|
|
134
|
+
if (workflowDefinition) {
|
|
135
|
+
tools['workflow'] = tool({
|
|
136
|
+
description: workflowDefinition.description,
|
|
137
|
+
inputSchema: jsonSchema(workflowDefinition.inputSchema),
|
|
138
|
+
execute: async (args: Record<string, unknown>, { toolCallId }: { toolCallId: string }) => {
|
|
139
|
+
const toolAbortController = interruptManager.registerToolExecution(sessionId, toolCallId);
|
|
140
|
+
try {
|
|
141
|
+
const workflowInput = {
|
|
142
|
+
prompt: args.prompt as string,
|
|
143
|
+
...(args.description ? { description: args.description as string } : {}),
|
|
144
|
+
...(args.subtasks ? { subtasks: args.subtasks as WorkflowInput['subtasks'] } : {}),
|
|
145
|
+
...(args.leafPreconfigId ? { leafPreconfigId: args.leafPreconfigId as string } : {}),
|
|
146
|
+
...(args.outputSchema ? { outputSchema: args.outputSchema as Record<string, unknown> } : {}),
|
|
147
|
+
} as WorkflowInput;
|
|
148
|
+
|
|
149
|
+
return await workflowPayload.execute(
|
|
150
|
+
workflowInput as unknown as Record<string, unknown>,
|
|
151
|
+
{
|
|
152
|
+
workspaceId,
|
|
153
|
+
sessionId,
|
|
154
|
+
ask: broadcastFn
|
|
155
|
+
? (ask: import('@capekai/tool').Ask) =>
|
|
156
|
+
createAskApi(sessionId, toolCallId, workflowPayload.name, broadcastFn, workspaceId, rootSessionId)(ask)
|
|
157
|
+
: async () => {
|
|
158
|
+
throw new Error('Cannot ask user: no broadcast channel available');
|
|
159
|
+
},
|
|
160
|
+
agentId,
|
|
161
|
+
workspacePath,
|
|
162
|
+
abortSignal: toolAbortController.signal,
|
|
163
|
+
allowedSubagentIds: workflowDefinition.allowedSubagentIds,
|
|
164
|
+
});
|
|
165
|
+
} finally {
|
|
166
|
+
interruptManager.unregisterToolExecution(sessionId, toolCallId);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Skill management tool ─────────────────────────────────
|
|
174
|
+
const skillManagePayload = domainPayload('skill_manage');
|
|
175
|
+
const skillSettings = workspace.settings?.skills;
|
|
176
|
+
if (skillManagePayload && skillSettings?.managementEnabled) {
|
|
177
|
+
const permissionRisk = skillSettings.permissionRisk;
|
|
178
|
+
const skillManageDefinition = await skillManagePayload.resolveDefinition?.(sessionId, {
|
|
179
|
+
workspacePath,
|
|
180
|
+
});
|
|
181
|
+
if (skillManageDefinition) {
|
|
182
|
+
tools['skill_manage'] = tool({
|
|
183
|
+
description: skillManageDefinition.description,
|
|
184
|
+
inputSchema: jsonSchema(skillManageDefinition.inputSchema),
|
|
185
|
+
execute: async (args: Record<string, unknown>, { toolCallId }: { toolCallId: string }) => {
|
|
186
|
+
const _toolAbortController = interruptManager.registerToolExecution(sessionId, toolCallId);
|
|
187
|
+
try {
|
|
188
|
+
const askApi = createAskApiOrThrow(sessionId, toolCallId, 'skill_manage', broadcastFn, workspaceId, rootSessionId);
|
|
189
|
+
const result = await skillManagePayload.execute(args, {
|
|
190
|
+
workspaceId,
|
|
191
|
+
sessionId,
|
|
192
|
+
ask: (ask: import('@capekai/tool').Ask) => askApi(ask),
|
|
193
|
+
agentId,
|
|
194
|
+
workspacePath,
|
|
195
|
+
permissionRisk,
|
|
196
|
+
});
|
|
197
|
+
return mergeDomainToolVisualization(skillManagePayload, args, result);
|
|
198
|
+
} finally {
|
|
199
|
+
interruptManager.unregisterToolExecution(sessionId, toolCallId);
|
|
200
|
+
await rejectPendingAsksByToolCallId(toolCallId);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── Session search tool ───────────────────────────────────
|
|
208
|
+
// The domain payload owns the settings gate (isEnabled) and the executor;
|
|
209
|
+
// the settings values the executor needs are captured here at build time
|
|
210
|
+
// exactly like pre-C5 and passed through the execution context.
|
|
211
|
+
const sessionSearchPayload = domainPayload('session_search');
|
|
212
|
+
const searchSettings = workspace.settings?.sessionSearch;
|
|
213
|
+
if (sessionSearchPayload && await sessionSearchPayload.isEnabled?.(workspaceId, sessionId) === true) {
|
|
214
|
+
tools['session_search'] = tool({
|
|
215
|
+
description: sessionSearchPayload.description,
|
|
216
|
+
inputSchema: jsonSchema(sessionSearchPayload.inputSchema as Record<string, unknown>),
|
|
217
|
+
execute: async (args: Record<string, unknown>, { toolCallId }: { toolCallId: string }) => {
|
|
218
|
+
const _toolAbortController = interruptManager.registerToolExecution(sessionId, toolCallId);
|
|
219
|
+
try {
|
|
220
|
+
const askApi = createAskApiOrThrow(sessionId, toolCallId, sessionSearchPayload.name, broadcastFn, workspaceId, rootSessionId);
|
|
221
|
+
const result = await sessionSearchPayload.execute(
|
|
222
|
+
args,
|
|
223
|
+
{
|
|
224
|
+
workspaceId,
|
|
225
|
+
sessionId,
|
|
226
|
+
ask: (ask: import('@capekai/tool').Ask) => askApi(ask),
|
|
227
|
+
agentId,
|
|
228
|
+
permissionRisk: searchSettings?.permissionRisk,
|
|
229
|
+
includeToolResults: searchSettings?.includeToolResults === true,
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
return mergeDomainToolVisualization(sessionSearchPayload, args, result);
|
|
233
|
+
} finally {
|
|
234
|
+
interruptManager.unregisterToolExecution(sessionId, toolCallId);
|
|
235
|
+
await rejectPendingAsksByToolCallId(toolCallId);
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Scheduler tool ────────────────────────────────────────
|
|
242
|
+
// Same generic domain seam as session_search: the domain payload owns the
|
|
243
|
+
// workspace settings gate plus the current-session scheduled-job recursion
|
|
244
|
+
// gate (isEnabled) and the executor; the permission risk is captured here
|
|
245
|
+
// at build time exactly like pre-C5 and passed through the execution
|
|
246
|
+
// context.
|
|
247
|
+
const schedulerPayload = domainPayload('scheduler');
|
|
248
|
+
if (schedulerPayload && await schedulerPayload.isEnabled?.(workspaceId, sessionId) === true) {
|
|
249
|
+
const schedulingRisk: PermissionRiskLevel = workspace.settings?.scheduling?.permissionRisk ?? 'none';
|
|
250
|
+
tools['scheduler'] = tool({
|
|
251
|
+
description: schedulerPayload.description,
|
|
252
|
+
inputSchema: jsonSchema(schedulerPayload.inputSchema as Record<string, unknown>),
|
|
253
|
+
execute: async (args: Record<string, unknown>, { toolCallId }: { toolCallId: string }) => {
|
|
254
|
+
const _toolAbortController = interruptManager.registerToolExecution(sessionId, toolCallId);
|
|
255
|
+
try {
|
|
256
|
+
const askApi = createAskApiOrThrow(sessionId, toolCallId, schedulerPayload.name, broadcastFn, workspaceId, rootSessionId);
|
|
257
|
+
const result = await schedulerPayload.execute(
|
|
258
|
+
args,
|
|
259
|
+
{
|
|
260
|
+
workspaceId,
|
|
261
|
+
sessionId,
|
|
262
|
+
ask: (ask: import('@capekai/tool').Ask) => askApi(ask),
|
|
263
|
+
agentId,
|
|
264
|
+
permissionRisk: schedulingRisk,
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
return mergeDomainToolVisualization(schedulerPayload, args, result);
|
|
268
|
+
} finally {
|
|
269
|
+
interruptManager.unregisterToolExecution(sessionId, toolCallId);
|
|
270
|
+
await rejectPendingAsksByToolCallId(toolCallId);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return tools;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Shared helpers ───────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
function createAskApiOrThrow(
|
|
282
|
+
sessionId: string,
|
|
283
|
+
toolCallId: string,
|
|
284
|
+
toolName: string,
|
|
285
|
+
broadcastFn: AskBroadcastFn | undefined,
|
|
286
|
+
workspaceId: string | undefined,
|
|
287
|
+
rootSessionId: string,
|
|
288
|
+
): import('@capekai/tool').AskApi {
|
|
289
|
+
if (!broadcastFn) {
|
|
290
|
+
throw new Error('Cannot ask user: no broadcast channel available');
|
|
291
|
+
}
|
|
292
|
+
return createAskApi(sessionId, toolCallId, toolName, broadcastFn, workspaceId, rootSessionId);
|
|
293
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Session } from '@capekai/types';
|
|
2
|
+
import { getSession } from '../storage/runtime';
|
|
3
|
+
|
|
4
|
+
export type ToolExecutionScope = 'subsession' | 'scheduled';
|
|
5
|
+
|
|
6
|
+
const RESTRICTED_CAPABILITIES: Record<ToolExecutionScope, ReadonlySet<string>> = {
|
|
7
|
+
subsession: new Set(['interactive-user-input']),
|
|
8
|
+
scheduled: new Set(['interactive-user-input']),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export async function resolveToolExecutionScopes(
|
|
12
|
+
sessionId: string,
|
|
13
|
+
sessionLookup: (id: string) => Session | null | Promise<Session | null> = getSession,
|
|
14
|
+
): Promise<ReadonlySet<ToolExecutionScope>> {
|
|
15
|
+
const scopes = new Set<ToolExecutionScope>();
|
|
16
|
+
const visited = new Set<string>();
|
|
17
|
+
let current = await sessionLookup(sessionId);
|
|
18
|
+
|
|
19
|
+
if (!current) {
|
|
20
|
+
return scopes;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (current.parentId) {
|
|
24
|
+
scopes.add('subsession');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
while (current) {
|
|
28
|
+
if (visited.has(current.id)) {
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
visited.add(current.id);
|
|
32
|
+
|
|
33
|
+
if (!current.parentId) {
|
|
34
|
+
const scheduledJobId = current.metadata?.scheduledJobId;
|
|
35
|
+
if (typeof scheduledJobId === 'string' && scheduledJobId.length > 0) {
|
|
36
|
+
scopes.add('scheduled');
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
current = await sessionLookup(current.parentId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return scopes;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isToolAllowedInContext(
|
|
48
|
+
capabilities: readonly string[] | undefined,
|
|
49
|
+
scopes: ReadonlySet<ToolExecutionScope>,
|
|
50
|
+
): boolean {
|
|
51
|
+
if (!capabilities || capabilities.length === 0) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const scope of scopes) {
|
|
56
|
+
const restricted = RESTRICTED_CAPABILITIES[scope];
|
|
57
|
+
for (const capability of capabilities) {
|
|
58
|
+
if (restricted.has(capability)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { BroadcastFn, BroadcastSessionFn } from '../runtime/host';
|
|
2
|
+
import type { GoalEvaluation, MessageWithParts, TextPart, ToolPart } from '@capekai/types';
|
|
3
|
+
import { listMessagesWithParts } from '../storage/runtime';
|
|
4
|
+
import { runOrchestratorSession } from '../workflow/orchestrator-session';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Goal domain: the goal evaluator model turn. Moved byte-for-byte from
|
|
8
|
+
* `core/goal-evaluator.ts`; the unscoped export keeps the pre-C5 module
|
|
9
|
+
* path (module storage plus the pinned core orchestrator-session
|
|
10
|
+
* compatibility forwarder), and `evaluateGoalWithDeps` runs against the
|
|
11
|
+
* injected transcript listing and orchestrator turn contract captured by
|
|
12
|
+
* the goal domain plugin. The composed path never imports workflow
|
|
13
|
+
* implementation code: the plugin bridges the shared
|
|
14
|
+
* `capek.orchestrator-session` contract into these structural types.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const MAX_TRANSCRIPT_MESSAGES = 20;
|
|
18
|
+
const MAX_TOOL_OUTPUT_CHARS = 500;
|
|
19
|
+
|
|
20
|
+
function summarizeToolState(toolPart: ToolPart): string {
|
|
21
|
+
const state = toolPart.state;
|
|
22
|
+
if (state.status === 'completed') {
|
|
23
|
+
if (typeof state.output === 'string') return state.output.slice(0, MAX_TOOL_OUTPUT_CHARS);
|
|
24
|
+
if (state.output && typeof state.output === 'object') return JSON.stringify(state.output).slice(0, MAX_TOOL_OUTPUT_CHARS);
|
|
25
|
+
return '(completed)';
|
|
26
|
+
}
|
|
27
|
+
if (state.status === 'error') return `ERROR: ${state.error ?? 'unknown'}`;
|
|
28
|
+
return `(${state.status})`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function buildTranscriptSummary(
|
|
32
|
+
listTranscript: (sessionId: string) => MessageWithParts[] | Promise<MessageWithParts[]>,
|
|
33
|
+
sessionId: string,
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
const messages = await listTranscript(sessionId);
|
|
36
|
+
return messages.slice(-MAX_TRANSCRIPT_MESSAGES).map((entry) => {
|
|
37
|
+
if (entry.message.role === 'user') {
|
|
38
|
+
const text = entry.parts
|
|
39
|
+
.filter((part): part is TextPart => part.type === 'text')
|
|
40
|
+
.map((part) => part.text || '')
|
|
41
|
+
.join('');
|
|
42
|
+
return text ? `[USER]: ${text}` : '';
|
|
43
|
+
}
|
|
44
|
+
if (entry.message.role === 'assistant') {
|
|
45
|
+
return entry.parts.map((part) => {
|
|
46
|
+
if (part.type === 'text' && part.text) return `[ASSISTANT]: ${part.text}`;
|
|
47
|
+
if (part.type === 'tool') return `[TOOL: ${part.name}]: ${summarizeToolState(part)}`;
|
|
48
|
+
return '';
|
|
49
|
+
}).filter(Boolean).join('\n');
|
|
50
|
+
}
|
|
51
|
+
return '';
|
|
52
|
+
}).filter(Boolean).join('\n\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Structural copy of the shared orchestrator-session contract result. The
|
|
56
|
+
* goal domain never imports workflow implementation code; the plugin
|
|
57
|
+
* bridges the named `capek.orchestrator-session` contract into this shape. */
|
|
58
|
+
export interface GoalOrchestratorTurnOptions {
|
|
59
|
+
parentSessionId: string;
|
|
60
|
+
title: string;
|
|
61
|
+
agentName: string;
|
|
62
|
+
systemPrompt: string;
|
|
63
|
+
userPrompt: string;
|
|
64
|
+
maxTokens?: number;
|
|
65
|
+
abortSignal?: AbortSignal;
|
|
66
|
+
broadcast?: BroadcastFn;
|
|
67
|
+
broadcastSessionCreated?: BroadcastSessionFn;
|
|
68
|
+
broadcastSessionUpdated?: BroadcastSessionFn;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface GoalOrchestratorTurnResult {
|
|
72
|
+
text: string;
|
|
73
|
+
json: Record<string, unknown> | null;
|
|
74
|
+
sessionId: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface GoalEvaluatorDeps {
|
|
78
|
+
listTranscript(sessionId: string): MessageWithParts[] | Promise<MessageWithParts[]>;
|
|
79
|
+
orchestrator: {
|
|
80
|
+
run(options: GoalOrchestratorTurnOptions): Promise<GoalOrchestratorTurnResult>;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface EvaluateGoalOptions {
|
|
85
|
+
sessionId: string;
|
|
86
|
+
condition: string;
|
|
87
|
+
turn: number;
|
|
88
|
+
maxTurns: number;
|
|
89
|
+
abortSignal?: AbortSignal;
|
|
90
|
+
broadcast?: BroadcastFn;
|
|
91
|
+
broadcastSessionCreated?: BroadcastSessionFn;
|
|
92
|
+
broadcastSessionUpdated?: BroadcastSessionFn;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Unscoped evaluator: module storage transcript plus the pinned core
|
|
96
|
+
* orchestrator-session forwarder, exactly like the pre-C5 path. */
|
|
97
|
+
export async function evaluateGoal(options: EvaluateGoalOptions): Promise<GoalEvaluation> {
|
|
98
|
+
return evaluateGoalWithDeps(options, {
|
|
99
|
+
listTranscript: listMessagesWithParts,
|
|
100
|
+
orchestrator: { run: runOrchestratorSession },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Composed evaluator over the injected transcript listing and orchestrator
|
|
105
|
+
* turn contract. */
|
|
106
|
+
export async function evaluateGoalWithDeps(
|
|
107
|
+
options: EvaluateGoalOptions,
|
|
108
|
+
deps: GoalEvaluatorDeps,
|
|
109
|
+
): Promise<GoalEvaluation> {
|
|
110
|
+
console.log('[goal:evaluator] Starting evaluation', {
|
|
111
|
+
sessionId: options.sessionId,
|
|
112
|
+
turn: options.turn,
|
|
113
|
+
conditionPreview: options.condition.slice(0, 80),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const transcript = await buildTranscriptSummary(deps.listTranscript, options.sessionId);
|
|
117
|
+
const system = [
|
|
118
|
+
'You are a goal evaluator. Your job is to determine if a completion condition',
|
|
119
|
+
'has been met based on the conversation transcript of an AI agent working on a task.',
|
|
120
|
+
'',
|
|
121
|
+
`Completion condition: "${options.condition}"`,
|
|
122
|
+
'',
|
|
123
|
+
'Rules:',
|
|
124
|
+
'- Look for evidence in tool outputs (test results, lint output, build status, file contents).',
|
|
125
|
+
'- Only return goalMet: true if you find CONCRETE evidence the condition is satisfied.',
|
|
126
|
+
'- Do NOT assume the condition is met just because the agent said it was — verify from tool outputs.',
|
|
127
|
+
'- If the condition requires tests to pass, look for actual test output showing all tests passing.',
|
|
128
|
+
'',
|
|
129
|
+
`Conversation transcript (turn ${options.turn} of ${options.maxTurns}):`,
|
|
130
|
+
transcript,
|
|
131
|
+
'',
|
|
132
|
+
'Respond with ONLY valid JSON (no markdown fences, no extra text):',
|
|
133
|
+
'{"goalMet": true/false, "reason": "explanation", "remainingWork": "what is left to do"}',
|
|
134
|
+
].join('\n');
|
|
135
|
+
const result = await deps.orchestrator.run({
|
|
136
|
+
parentSessionId: options.sessionId,
|
|
137
|
+
title: `Goal Eval (Turn ${options.turn}): ${options.condition.slice(0, 40)}`,
|
|
138
|
+
agentName: 'goal-evaluator',
|
|
139
|
+
systemPrompt: system,
|
|
140
|
+
userPrompt: `Evaluate: has the condition "${options.condition}" been met based on the transcript above?`,
|
|
141
|
+
maxTokens: 2048,
|
|
142
|
+
abortSignal: options.abortSignal,
|
|
143
|
+
broadcast: options.broadcast,
|
|
144
|
+
broadcastSessionCreated: options.broadcastSessionCreated,
|
|
145
|
+
broadcastSessionUpdated: options.broadcastSessionUpdated,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
console.log('[goal:evaluator] Evaluation complete', {
|
|
149
|
+
goalMet: result.json?.goalMet === true,
|
|
150
|
+
reason: result.json?.reason,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
goalMet: result.json?.goalMet === true,
|
|
155
|
+
reason: (result.json?.reason as string) ?? 'No reason provided',
|
|
156
|
+
remainingWork: (result.json?.remainingWork as string) ?? undefined,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The run directive for the next turn when the goal is not yet met. Moved
|
|
161
|
+
* byte-for-byte from the pre-C5 goal-evaluator export. */
|
|
162
|
+
export function buildContinuationMessage(condition: string, reason: string, remainingWork?: string): string {
|
|
163
|
+
return [
|
|
164
|
+
`The goal is NOT yet met: ${condition}`,
|
|
165
|
+
'',
|
|
166
|
+
`Evaluator feedback: ${reason}`,
|
|
167
|
+
remainingWork ? `\nRemaining work: ${remainingWork}` : '',
|
|
168
|
+
'',
|
|
169
|
+
'Continue working toward the goal. Do not repeat work you have already done.',
|
|
170
|
+
].join('\n');
|
|
171
|
+
}
|