@rubytech/taskmaster 1.0.10 → 1.0.12
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/dist/agents/pi-embedded-helpers/errors.js +6 -7
- package/dist/agents/pi-embedded-runner/compact.js +5 -0
- package/dist/agents/pi-embedded-runner/run/attempt.js +5 -0
- package/dist/agents/pi-embedded-runner/run/payloads.js +4 -4
- package/dist/agents/pi-embedded-runner/run.js +4 -4
- package/dist/agents/pi-tools.js +1 -0
- package/dist/agents/skills/workspace.js +40 -3
- package/dist/agents/system-prompt.js +12 -6
- package/dist/agents/taskmaster-tools.js +10 -0
- package/dist/agents/tool-display.json +5 -0
- package/dist/agents/tool-policy.js +1 -0
- package/dist/agents/tools/memory-reindex-tool.js +67 -0
- package/dist/agents/tools/skill-read-tool.js +75 -0
- package/dist/agents/workspace.js +2 -2
- package/dist/auto-reply/reply/agent-runner-execution.js +17 -40
- package/dist/auto-reply/reply/agent-runner.js +14 -24
- package/dist/auto-reply/reply/session.js +1 -1
- package/dist/build-info.json +3 -3
- package/dist/cli/gateway-cli/dev.js +1 -1
- package/dist/cli/provision-seed.js +1 -0
- package/dist/control-ui/assets/{index-Dcwiyz6o.js → index-D8ayJUWC.js} +205 -195
- package/dist/control-ui/assets/index-D8ayJUWC.js.map +1 -0
- package/dist/control-ui/assets/{index-DE-53MKi.css → index-dMMqL7A5.css} +1 -1
- package/dist/control-ui/index.html +2 -2
- package/dist/gateway/protocol/schema/sessions-transcript.js +1 -0
- package/dist/gateway/server-methods/memory.js +62 -0
- package/dist/gateway/server-methods/sessions-transcript.js +10 -9
- package/dist/gateway/server-methods.js +5 -1
- package/dist/memory/manager.js +17 -1
- package/dist/memory/memory-schema.js +23 -1
- package/dist/web/auto-reply/monitor.js +1 -1
- package/package.json +1 -1
- package/taskmaster-docs/USER-GUIDE.md +43 -12
- package/templates/customer/agents/admin/AGENTS.md +6 -5
- package/templates/customer/agents/admin/TOOLS.md +36 -0
- package/templates/taskmaster/agents/admin/AGENTS.md +7 -5
- package/templates/tradesupport/agents/admin/AGENTS.md +6 -5
- package/dist/control-ui/assets/index-Dcwiyz6o.js.map +0 -1
|
@@ -248,10 +248,10 @@ export function formatAssistantErrorText(msg, opts) {
|
|
|
248
248
|
return ("Context overflow: prompt too large for the model. " +
|
|
249
249
|
"Try again with less input or a larger-context model.");
|
|
250
250
|
}
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
251
|
+
// Conversation history corruption — role ordering, orphaned tool results, etc.
|
|
252
|
+
// The session is stuck; every subsequent message will fail until reset.
|
|
253
|
+
if (/incorrect role information|roles must alternate|400.*role|"message".*role.*information|tool_use_id|tool_result.*tool_use|tool_use.*tool_result/i.test(raw)) {
|
|
254
|
+
return "This conversation got into a bad state. Use /new to start fresh.";
|
|
255
255
|
}
|
|
256
256
|
const invalidRequest = raw.match(/"type":"invalid_request_error".*?"message":"([^"]+)"/);
|
|
257
257
|
if (invalidRequest?.[1]) {
|
|
@@ -276,9 +276,8 @@ export function sanitizeUserFacingText(text) {
|
|
|
276
276
|
const trimmed = stripped.trim();
|
|
277
277
|
if (!trimmed)
|
|
278
278
|
return stripped;
|
|
279
|
-
if (/incorrect role information|roles must alternate/i.test(trimmed)) {
|
|
280
|
-
return
|
|
281
|
-
"If this persists, use /new to start a fresh session.");
|
|
279
|
+
if (/incorrect role information|roles must alternate|tool_use_id|tool_result.*tool_use|tool_use.*tool_result/i.test(trimmed)) {
|
|
280
|
+
return "This conversation got into a bad state. Use /new to start fresh.";
|
|
282
281
|
}
|
|
283
282
|
if (isContextOverflowError(trimmed)) {
|
|
284
283
|
return ("Context overflow: prompt too large for the model. " +
|
|
@@ -138,6 +138,10 @@ export async function compactEmbeddedPiSessionDirect(params) {
|
|
|
138
138
|
sessionId: params.sessionId,
|
|
139
139
|
warn: makeBootstrapWarn({ sessionLabel, warn: (message) => log.warn(message) }),
|
|
140
140
|
});
|
|
141
|
+
// Extract skill base dirs for scoped skill_read tool
|
|
142
|
+
const skillBaseDirs = (params.skillsSnapshot?.resolvedSkills ?? [])
|
|
143
|
+
.map((s) => s.baseDir)
|
|
144
|
+
.filter(Boolean);
|
|
141
145
|
const runAbortController = new AbortController();
|
|
142
146
|
const toolsRaw = createTaskmasterCodingTools({
|
|
143
147
|
exec: {
|
|
@@ -159,6 +163,7 @@ export async function compactEmbeddedPiSessionDirect(params) {
|
|
|
159
163
|
modelProvider: model.provider,
|
|
160
164
|
modelId,
|
|
161
165
|
modelAuthMode: resolveModelAuthMode(model.provider, params.config),
|
|
166
|
+
skillBaseDirs,
|
|
162
167
|
});
|
|
163
168
|
const tools = sanitizeToolsForGoogle({ tools: toolsRaw, provider });
|
|
164
169
|
logToolSchemasForGoogle({ tools, provider });
|
|
@@ -143,6 +143,10 @@ export async function runEmbeddedAttempt(params) {
|
|
|
143
143
|
? ["Reminder: commit your changes in this workspace after edits."]
|
|
144
144
|
: undefined;
|
|
145
145
|
const agentDir = params.agentDir ?? resolveTaskmasterAgentDir();
|
|
146
|
+
// Extract skill base dirs for scoped skill_read tool
|
|
147
|
+
const skillBaseDirs = (params.skillsSnapshot?.resolvedSkills ?? [])
|
|
148
|
+
.map((s) => s.baseDir)
|
|
149
|
+
.filter(Boolean);
|
|
146
150
|
// Check if the model supports native image input
|
|
147
151
|
const modelHasVision = params.model.input?.includes("image") ?? false;
|
|
148
152
|
const toolsRaw = params.disableTools
|
|
@@ -175,6 +179,7 @@ export async function runEmbeddedAttempt(params) {
|
|
|
175
179
|
hasRepliedRef: params.hasRepliedRef,
|
|
176
180
|
modelHasVision,
|
|
177
181
|
skipMessageDelivery: params.skipMessageDelivery,
|
|
182
|
+
skillBaseDirs,
|
|
178
183
|
});
|
|
179
184
|
const tools = sanitizeToolsForGoogle({ tools: toolsRaw, provider: params.provider });
|
|
180
185
|
logToolSchemasForGoogle({ tools, provider: params.provider });
|
|
@@ -30,10 +30,10 @@ export function buildEmbeddedRunPayloads(params) {
|
|
|
30
30
|
: null;
|
|
31
31
|
const normalizedErrorText = errorText ? normalizeTextForComparison(errorText) : null;
|
|
32
32
|
const genericErrorText = "The AI service returned an error. Please try again.";
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
replyItems.push({ text:
|
|
33
|
+
// Public-facing agents: silently swallow errors — no error text of any kind.
|
|
34
|
+
// Admin agent: show the formatted error from formatAssistantErrorText.
|
|
35
|
+
if (errorText && !params.suppressRawErrors) {
|
|
36
|
+
replyItems.push({ text: errorText, isError: true });
|
|
37
37
|
}
|
|
38
38
|
const inlineToolResults = params.inlineToolResultsAllowed && params.verboseLevel !== "off" && params.toolMetas.length > 0;
|
|
39
39
|
if (inlineToolResults) {
|
|
@@ -331,13 +331,13 @@ export async function runEmbeddedPiAgent(params) {
|
|
|
331
331
|
},
|
|
332
332
|
};
|
|
333
333
|
}
|
|
334
|
-
// Handle
|
|
335
|
-
|
|
334
|
+
// Handle session corruption errors (role ordering, orphaned tool results)
|
|
335
|
+
// Returning kind: "role_ordering" triggers auto-recovery (session reset) upstream.
|
|
336
|
+
if (/incorrect role information|roles must alternate|tool_use_id|tool_result.*tool_use|tool_use.*tool_result/i.test(errorText)) {
|
|
336
337
|
return {
|
|
337
338
|
payloads: [
|
|
338
339
|
{
|
|
339
|
-
text: "
|
|
340
|
-
"If this persists, use /new to start a fresh session.",
|
|
340
|
+
text: "This conversation got into a bad state — resetting now.",
|
|
341
341
|
isError: true,
|
|
342
342
|
},
|
|
343
343
|
],
|
package/dist/agents/pi-tools.js
CHANGED
|
@@ -218,6 +218,7 @@ export function createTaskmasterCodingTools(options) {
|
|
|
218
218
|
modelHasVision: options?.modelHasVision,
|
|
219
219
|
requesterAgentIdOverride: agentId,
|
|
220
220
|
messageSkipDelivery: options?.skipMessageDelivery,
|
|
221
|
+
skillBaseDirs: options?.skillBaseDirs,
|
|
221
222
|
}),
|
|
222
223
|
];
|
|
223
224
|
const coreToolNames = new Set(tools
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { loadSkillsFromDir } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
|
5
5
|
import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
|
|
6
6
|
import { resolveBundledSkillsDir } from "./bundled-dir.js";
|
|
@@ -10,6 +10,41 @@ import { resolvePluginSkillDirs } from "./plugin-skills.js";
|
|
|
10
10
|
import { serializeByKey } from "./serialize.js";
|
|
11
11
|
const fsp = fs.promises;
|
|
12
12
|
const skillsLogger = createSubsystemLogger("skills");
|
|
13
|
+
function escapeXml(str) {
|
|
14
|
+
return str
|
|
15
|
+
.replace(/&/g, "&")
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/"/g, """)
|
|
19
|
+
.replace(/'/g, "'");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Format skills for inclusion in a system prompt.
|
|
23
|
+
* Identical to the library's formatSkillsForPrompt except it references
|
|
24
|
+
* `skill_read` instead of `read` so agents denied group:fs can still
|
|
25
|
+
* load skill content.
|
|
26
|
+
*/
|
|
27
|
+
function formatSkillsForPromptTaskmaster(skills) {
|
|
28
|
+
const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
|
|
29
|
+
if (visibleSkills.length === 0)
|
|
30
|
+
return "";
|
|
31
|
+
const lines = [
|
|
32
|
+
"\n\nThe following skills provide specialized instructions for specific tasks.",
|
|
33
|
+
"Use the skill_read tool to load a skill's file when the task matches its description.",
|
|
34
|
+
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
|
|
35
|
+
"",
|
|
36
|
+
"<available_skills>",
|
|
37
|
+
];
|
|
38
|
+
for (const skill of visibleSkills) {
|
|
39
|
+
lines.push(" <skill>");
|
|
40
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
41
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
42
|
+
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
|
|
43
|
+
lines.push(" </skill>");
|
|
44
|
+
}
|
|
45
|
+
lines.push("</available_skills>");
|
|
46
|
+
return lines.join("\n");
|
|
47
|
+
}
|
|
13
48
|
const skillCommandDebugOnce = new Set();
|
|
14
49
|
function debugSkillCommandOnce(messageKey, message, meta) {
|
|
15
50
|
if (skillCommandDebugOnce.has(messageKey))
|
|
@@ -141,7 +176,9 @@ export function buildWorkspaceSkillSnapshot(workspaceDir, opts) {
|
|
|
141
176
|
const promptEntries = eligible.filter((entry) => entry.invocation?.disableModelInvocation !== true);
|
|
142
177
|
const resolvedSkills = promptEntries.map((entry) => entry.skill);
|
|
143
178
|
const remoteNote = opts?.eligibility?.remote?.note?.trim();
|
|
144
|
-
const prompt = [remoteNote,
|
|
179
|
+
const prompt = [remoteNote, formatSkillsForPromptTaskmaster(resolvedSkills)]
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.join("\n");
|
|
145
182
|
return {
|
|
146
183
|
prompt,
|
|
147
184
|
skills: eligible.map((entry) => ({
|
|
@@ -157,7 +194,7 @@ export function buildWorkspaceSkillsPrompt(workspaceDir, opts) {
|
|
|
157
194
|
const eligible = filterSkillEntries(skillEntries, opts?.config, opts?.skillFilter, opts?.eligibility);
|
|
158
195
|
const promptEntries = eligible.filter((entry) => entry.invocation?.disableModelInvocation !== true);
|
|
159
196
|
const remoteNote = opts?.eligibility?.remote?.note?.trim();
|
|
160
|
-
return [remoteNote,
|
|
197
|
+
return [remoteNote, formatSkillsForPromptTaskmaster(promptEntries.map((entry) => entry.skill))]
|
|
161
198
|
.filter(Boolean)
|
|
162
199
|
.join("\n");
|
|
163
200
|
}
|
|
@@ -132,9 +132,11 @@ export function buildAgentSystemPrompt(params) {
|
|
|
132
132
|
sessions_spawn: "Spawn a sub-agent session",
|
|
133
133
|
session_status: "Show a /status-equivalent status card (usage + time + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override",
|
|
134
134
|
image: "Analyze an image with the configured image model",
|
|
135
|
+
skill_read: "Read files within skill directories (SKILL.md, references)",
|
|
135
136
|
};
|
|
136
137
|
const toolOrder = [
|
|
137
138
|
"read",
|
|
139
|
+
"skill_read",
|
|
138
140
|
"write",
|
|
139
141
|
"edit",
|
|
140
142
|
"apply_patch",
|
|
@@ -192,6 +194,10 @@ export function buildAgentSystemPrompt(params) {
|
|
|
192
194
|
}
|
|
193
195
|
const hasGateway = availableTools.has("gateway");
|
|
194
196
|
const readToolName = resolveToolName("read");
|
|
197
|
+
// For skills section: prefer skill_read when agent has it but not read
|
|
198
|
+
const skillReadToolName = availableTools.has("skill_read") && !availableTools.has("read")
|
|
199
|
+
? resolveToolName("skill_read")
|
|
200
|
+
: readToolName;
|
|
195
201
|
const execToolName = resolveToolName("exec");
|
|
196
202
|
const processToolName = resolveToolName("process");
|
|
197
203
|
const extraSystemPrompt = params.extraSystemPrompt?.trim();
|
|
@@ -231,7 +237,7 @@ export function buildAgentSystemPrompt(params) {
|
|
|
231
237
|
const skillsSection = buildSkillsSection({
|
|
232
238
|
skillsPrompt,
|
|
233
239
|
isMinimal,
|
|
234
|
-
readToolName,
|
|
240
|
+
readToolName: skillReadToolName,
|
|
235
241
|
});
|
|
236
242
|
const memorySection = buildMemorySection({ isMinimal, availableTools });
|
|
237
243
|
const docsSection = buildDocsSection({
|
|
@@ -272,11 +278,11 @@ export function buildAgentSystemPrompt(params) {
|
|
|
272
278
|
"If a task is more complex or takes longer, spawn a sub-agent. It will do the work for you and ping you when it's done. You can always check up on it.",
|
|
273
279
|
"",
|
|
274
280
|
"## Tool Call Style",
|
|
275
|
-
"
|
|
276
|
-
"
|
|
277
|
-
"
|
|
278
|
-
"
|
|
279
|
-
"Never
|
|
281
|
+
"Every text block you produce is delivered as a separate message. Unnecessary text blocks = unnecessary messages sent to the user.",
|
|
282
|
+
"Call tools silently by default. Do not announce what you are about to do, explain what you just did, or summarise completed steps.",
|
|
283
|
+
"If a task requires multiple tool calls, execute them without commentary. The user sees only your final response.",
|
|
284
|
+
"Narrate only when the user needs to know why there is a delay, or when an action has consequences they should be aware of (e.g. a deletion or sending on their behalf).",
|
|
285
|
+
"Never produce process narration like 'I'll check...', 'Let me look up...', 'Perfect — found it', 'Done — I've sent...'. These become unwanted messages.",
|
|
280
286
|
"",
|
|
281
287
|
"## Taskmaster CLI Quick Reference",
|
|
282
288
|
"Taskmaster is controlled via subcommands. Do not invent commands.",
|
|
@@ -8,6 +8,7 @@ import { createDocumentTool } from "./tools/document-tool.js";
|
|
|
8
8
|
import { createGatewayTool } from "./tools/gateway-tool.js";
|
|
9
9
|
import { createImageTool } from "./tools/image-tool.js";
|
|
10
10
|
import { createMemoryGetTool, createMemorySaveMediaTool, createMemorySearchTool, createMemoryWriteTool, } from "./tools/memory-tool.js";
|
|
11
|
+
import { createMemoryReindexTool } from "./tools/memory-reindex-tool.js";
|
|
11
12
|
import { createMessageTool } from "./tools/message-tool.js";
|
|
12
13
|
import { createNodesTool } from "./tools/nodes-tool.js";
|
|
13
14
|
import { createSessionStatusTool } from "./tools/session-status-tool.js";
|
|
@@ -23,6 +24,7 @@ import { createLicenseGenerateTool } from "./tools/license-tool.js";
|
|
|
23
24
|
import { createCustomerLookupTool } from "./tools/customer-lookup-tool.js";
|
|
24
25
|
import { createCustomerUpdateTool } from "./tools/customer-update-tool.js";
|
|
25
26
|
import { createRelayMessageTool } from "./tools/relay-message-tool.js";
|
|
27
|
+
import { createSkillReadTool } from "./tools/skill-read-tool.js";
|
|
26
28
|
export function createTaskmasterTools(options) {
|
|
27
29
|
const imageTool = options?.agentDir?.trim()
|
|
28
30
|
? createImageTool({
|
|
@@ -127,6 +129,11 @@ export function createTaskmasterTools(options) {
|
|
|
127
129
|
createCustomerUpdateTool(),
|
|
128
130
|
createRelayMessageTool(),
|
|
129
131
|
];
|
|
132
|
+
// Add scoped skill_read tool when skill directories are provided
|
|
133
|
+
const skillDirs = (options?.skillBaseDirs ?? []).filter(Boolean);
|
|
134
|
+
if (skillDirs.length > 0) {
|
|
135
|
+
tools.push(createSkillReadTool({ allowedSkillDirs: skillDirs }));
|
|
136
|
+
}
|
|
130
137
|
// Add memory tools (conditionally based on config)
|
|
131
138
|
const memoryToolOptions = {
|
|
132
139
|
config: options?.config,
|
|
@@ -136,6 +143,7 @@ export function createTaskmasterTools(options) {
|
|
|
136
143
|
const memoryGetTool = createMemoryGetTool(memoryToolOptions);
|
|
137
144
|
const memoryWriteTool = createMemoryWriteTool(memoryToolOptions);
|
|
138
145
|
const memorySaveMediaTool = createMemorySaveMediaTool(memoryToolOptions);
|
|
146
|
+
const memoryReindexTool = createMemoryReindexTool(memoryToolOptions);
|
|
139
147
|
if (memorySearchTool)
|
|
140
148
|
tools.push(memorySearchTool);
|
|
141
149
|
if (memoryGetTool)
|
|
@@ -144,6 +152,8 @@ export function createTaskmasterTools(options) {
|
|
|
144
152
|
tools.push(memoryWriteTool);
|
|
145
153
|
if (memorySaveMediaTool)
|
|
146
154
|
tools.push(memorySaveMediaTool);
|
|
155
|
+
if (memoryReindexTool)
|
|
156
|
+
tools.push(memoryReindexTool);
|
|
147
157
|
const pluginTools = resolvePluginTools({
|
|
148
158
|
context: {
|
|
149
159
|
config: options?.config,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { getMemorySearchManager } from "../../memory/index.js";
|
|
3
|
+
import { resolveSessionAgentId } from "../agent-scope.js";
|
|
4
|
+
import { resolveMemorySearchConfig } from "../memory-search.js";
|
|
5
|
+
import { jsonResult, readStringParam } from "./common.js";
|
|
6
|
+
const MemoryReindexSchema = Type.Object({
|
|
7
|
+
action: Type.String({
|
|
8
|
+
description: "Action to perform: 'reindex' (force full reindex) or 'status' (return index stats)",
|
|
9
|
+
}),
|
|
10
|
+
});
|
|
11
|
+
export function createMemoryReindexTool(options) {
|
|
12
|
+
const cfg = options.config;
|
|
13
|
+
if (!cfg)
|
|
14
|
+
return null;
|
|
15
|
+
const agentId = resolveSessionAgentId({
|
|
16
|
+
sessionKey: options.agentSessionKey,
|
|
17
|
+
config: cfg,
|
|
18
|
+
});
|
|
19
|
+
if (!resolveMemorySearchConfig(cfg, agentId))
|
|
20
|
+
return null;
|
|
21
|
+
return {
|
|
22
|
+
label: "Memory Reindex",
|
|
23
|
+
name: "memory_reindex",
|
|
24
|
+
description: "Trigger a memory index rebuild or check index status. " +
|
|
25
|
+
"Use action='reindex' to force a full reindex of all memory files and sessions. " +
|
|
26
|
+
"Use action='status' to return current index stats (file counts, provider, sources).",
|
|
27
|
+
parameters: MemoryReindexSchema,
|
|
28
|
+
execute: async (_toolCallId, params) => {
|
|
29
|
+
const action = readStringParam(params, "action", { required: true });
|
|
30
|
+
const { manager, error } = await getMemorySearchManager({ cfg, agentId });
|
|
31
|
+
if (!manager) {
|
|
32
|
+
return jsonResult({ ok: false, error: error ?? "memory index unavailable" });
|
|
33
|
+
}
|
|
34
|
+
if (action === "status") {
|
|
35
|
+
const status = manager.status();
|
|
36
|
+
return jsonResult({
|
|
37
|
+
ok: true,
|
|
38
|
+
action: "status",
|
|
39
|
+
files: status.files,
|
|
40
|
+
chunks: status.chunks,
|
|
41
|
+
sources: status.sources,
|
|
42
|
+
sourceCounts: status.sourceCounts,
|
|
43
|
+
provider: status.provider,
|
|
44
|
+
model: status.model,
|
|
45
|
+
dirty: status.dirty,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// Default: reindex
|
|
49
|
+
try {
|
|
50
|
+
await manager.sync({ reason: "agent-tool", force: true });
|
|
51
|
+
const status = manager.status();
|
|
52
|
+
return jsonResult({
|
|
53
|
+
ok: true,
|
|
54
|
+
action: "reindex",
|
|
55
|
+
files: status.files,
|
|
56
|
+
chunks: status.chunks,
|
|
57
|
+
sources: status.sources,
|
|
58
|
+
sourceCounts: status.sourceCounts,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
63
|
+
return jsonResult({ ok: false, action: "reindex", error: message });
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { readStringParam } from "./common.js";
|
|
6
|
+
const SkillReadSchema = Type.Object({
|
|
7
|
+
path: Type.String({ description: "Absolute or relative path to the file to read" }),
|
|
8
|
+
skill_dir: Type.Optional(Type.String({
|
|
9
|
+
description: "Skill directory for resolving relative paths. If omitted, path must be absolute.",
|
|
10
|
+
})),
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Create a scoped file-read tool that only allows reading files within
|
|
14
|
+
* the provided skill directories. This gives agents access to skill
|
|
15
|
+
* content (SKILL.md, references/) without exposing the full filesystem.
|
|
16
|
+
*/
|
|
17
|
+
export function createSkillReadTool(opts) {
|
|
18
|
+
// Pre-resolve allowed dirs at creation time (async realpath done at call time).
|
|
19
|
+
const allowedDirs = opts.allowedSkillDirs.filter(Boolean);
|
|
20
|
+
return {
|
|
21
|
+
label: "Skill Read",
|
|
22
|
+
name: "skill_read",
|
|
23
|
+
description: "Read a file within a skill directory (SKILL.md, references, etc.). " +
|
|
24
|
+
"Use this to load skill content on demand. " +
|
|
25
|
+
"Only files inside skill directories are accessible.",
|
|
26
|
+
parameters: SkillReadSchema,
|
|
27
|
+
execute: async (_toolCallId, args) => {
|
|
28
|
+
const params = args;
|
|
29
|
+
const rawPath = readStringParam(params, "path", { required: true });
|
|
30
|
+
const skillDir = readStringParam(params, "skill_dir");
|
|
31
|
+
// Resolve the target path
|
|
32
|
+
let targetPath;
|
|
33
|
+
if (path.isAbsolute(rawPath)) {
|
|
34
|
+
targetPath = path.resolve(rawPath);
|
|
35
|
+
}
|
|
36
|
+
else if (skillDir) {
|
|
37
|
+
targetPath = path.resolve(skillDir, rawPath);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
throw new Error("Relative path requires skill_dir. Provide an absolute path or include skill_dir.");
|
|
41
|
+
}
|
|
42
|
+
// Security: resolve symlinks on the target and check containment
|
|
43
|
+
let realTarget;
|
|
44
|
+
try {
|
|
45
|
+
realTarget = await realpath(targetPath);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error(`File not found: ${targetPath}`);
|
|
49
|
+
}
|
|
50
|
+
let allowed = false;
|
|
51
|
+
for (const dir of allowedDirs) {
|
|
52
|
+
let realDir;
|
|
53
|
+
try {
|
|
54
|
+
realDir = await realpath(dir);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const prefix = realDir.endsWith("/") ? realDir : `${realDir}/`;
|
|
60
|
+
if (realTarget === realDir || realTarget.startsWith(prefix)) {
|
|
61
|
+
allowed = true;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!allowed) {
|
|
66
|
+
throw new Error(`Access denied: ${rawPath} is outside all skill directories.`);
|
|
67
|
+
}
|
|
68
|
+
const content = await fs.readFile(realTarget, "utf-8");
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: content }],
|
|
71
|
+
details: { path: realTarget },
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
package/dist/agents/workspace.js
CHANGED
|
@@ -20,7 +20,7 @@ export const DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";
|
|
|
20
20
|
export const DEFAULT_USER_FILENAME = "USER.md";
|
|
21
21
|
export const DEFAULT_HEARTBEAT_FILENAME = "HEARTBEAT.md";
|
|
22
22
|
export const DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
|
|
23
|
-
const TEMPLATE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../
|
|
23
|
+
const TEMPLATE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../templates/customer/agents/admin");
|
|
24
24
|
function stripFrontMatter(content) {
|
|
25
25
|
if (!content.startsWith("---"))
|
|
26
26
|
return content;
|
|
@@ -39,7 +39,7 @@ async function loadTemplate(name) {
|
|
|
39
39
|
return stripFrontMatter(content);
|
|
40
40
|
}
|
|
41
41
|
catch {
|
|
42
|
-
throw new Error(`Missing workspace template: ${name} (${templatePath}). Ensure
|
|
42
|
+
throw new Error(`Missing workspace template: ${name} (${templatePath}). Ensure templates/customer is packaged.`);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
async function writeFileIfMissing(filePath, content) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
2
|
import { resolveAgentModelFallbacksOverride, resolveDefaultAgentId, } from "../../agents/agent-scope.js";
|
|
4
3
|
import { runCliAgent } from "../../agents/cli-runner.js";
|
|
5
4
|
import { getCliSessionId } from "../../agents/cli-session.js";
|
|
@@ -7,7 +6,7 @@ import { runWithModelFallback } from "../../agents/model-fallback.js";
|
|
|
7
6
|
import { isCliProvider } from "../../agents/model-selection.js";
|
|
8
7
|
import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
|
|
9
8
|
import { isCompactionFailureError, isContextOverflowError, isLikelyContextOverflowError, sanitizeUserFacingText, } from "../../agents/pi-embedded-helpers.js";
|
|
10
|
-
import { resolveAgentIdFromSessionKey, resolveGroupSessionKey,
|
|
9
|
+
import { resolveAgentIdFromSessionKey, resolveGroupSessionKey, } from "../../config/sessions.js";
|
|
11
10
|
import { logVerbose } from "../../globals.js";
|
|
12
11
|
import { emitAgentEvent, registerAgentRunContext } from "../../infra/agent-events.js";
|
|
13
12
|
import { defaultRuntime } from "../../runtime.js";
|
|
@@ -198,7 +197,9 @@ export async function runAgentTurnWithFallback(params) {
|
|
|
198
197
|
ownerNumbers: params.followupRun.run.ownerNumbers,
|
|
199
198
|
enforceFinalTag: resolveEnforceFinalTag(params.followupRun.run, provider),
|
|
200
199
|
senderE164: params.sessionCtx.SenderE164,
|
|
201
|
-
// Suppress raw API errors for non-default agents (public-facing)
|
|
200
|
+
// Suppress raw API errors for non-default agents (public-facing).
|
|
201
|
+
// Default (admin) agent gets formatted error text from formatAssistantErrorText
|
|
202
|
+
// which provides human-readable messages for known error types.
|
|
202
203
|
suppressRawErrors: (() => {
|
|
203
204
|
const cfg = params.followupRun.run.config;
|
|
204
205
|
if (!cfg)
|
|
@@ -405,7 +406,7 @@ export async function runAgentTurnWithFallback(params) {
|
|
|
405
406
|
return {
|
|
406
407
|
kind: "final",
|
|
407
408
|
payload: {
|
|
408
|
-
text: "⚠️
|
|
409
|
+
text: "⚠️ Conversation got into a bad state. I've reset it - please try again.",
|
|
409
410
|
},
|
|
410
411
|
};
|
|
411
412
|
}
|
|
@@ -417,7 +418,7 @@ export async function runAgentTurnWithFallback(params) {
|
|
|
417
418
|
const isContextOverflow = isLikelyContextOverflowError(message);
|
|
418
419
|
const isCompactionFailure = isCompactionFailureError(message);
|
|
419
420
|
const isSessionCorruption = /function call turn comes immediately after/i.test(message);
|
|
420
|
-
const isRoleOrderingError = /incorrect role information|roles must alternate/i.test(message);
|
|
421
|
+
const isRoleOrderingError = /incorrect role information|roles must alternate|tool_use_id|tool_result.*tool_use|tool_use.*tool_result/i.test(message);
|
|
421
422
|
if (isCompactionFailure &&
|
|
422
423
|
!didResetAfterCompactionFailure &&
|
|
423
424
|
(await params.resetSessionAfterCompactionFailure(message))) {
|
|
@@ -435,52 +436,28 @@ export async function runAgentTurnWithFallback(params) {
|
|
|
435
436
|
return {
|
|
436
437
|
kind: "final",
|
|
437
438
|
payload: {
|
|
438
|
-
text: "⚠️
|
|
439
|
+
text: "⚠️ Conversation got into a bad state. I've reset it - please try again.",
|
|
439
440
|
},
|
|
440
441
|
};
|
|
441
442
|
}
|
|
442
443
|
}
|
|
443
444
|
// Auto-recover from Gemini session corruption by resetting the session
|
|
444
|
-
if (isSessionCorruption
|
|
445
|
-
params.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (corruptedSessionId) {
|
|
454
|
-
const transcriptPath = resolveSessionTranscriptPath(corruptedSessionId);
|
|
455
|
-
try {
|
|
456
|
-
fs.unlinkSync(transcriptPath);
|
|
457
|
-
}
|
|
458
|
-
catch {
|
|
459
|
-
// Ignore if file doesn't exist
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
// Keep the in-memory snapshot consistent with the on-disk store reset.
|
|
463
|
-
delete params.activeSessionStore[sessionKey];
|
|
464
|
-
// Remove session entry from store using a fresh, locked snapshot.
|
|
465
|
-
await updateSessionStore(params.storePath, (store) => {
|
|
466
|
-
delete store[sessionKey];
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
catch (cleanupErr) {
|
|
470
|
-
defaultRuntime.error(`Failed to reset corrupted session ${params.sessionKey}: ${String(cleanupErr)}`);
|
|
445
|
+
if (isSessionCorruption) {
|
|
446
|
+
const didReset = await params.resetSessionAfterRoleOrderingConflict(message);
|
|
447
|
+
if (didReset) {
|
|
448
|
+
return {
|
|
449
|
+
kind: "final",
|
|
450
|
+
payload: {
|
|
451
|
+
text: "⚠️ Conversation got into a bad state. I've reset it - please try again.",
|
|
452
|
+
},
|
|
453
|
+
};
|
|
471
454
|
}
|
|
472
|
-
return {
|
|
473
|
-
kind: "final",
|
|
474
|
-
payload: {
|
|
475
|
-
text: "⚠️ Session history was corrupted. I've reset the conversation - please try again!",
|
|
476
|
-
},
|
|
477
|
-
};
|
|
478
455
|
}
|
|
479
456
|
defaultRuntime.error(`Embedded agent failed before reply: ${message}`);
|
|
480
457
|
const fallbackText = isContextOverflow
|
|
481
458
|
? "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model."
|
|
482
459
|
: isRoleOrderingError
|
|
483
|
-
? "⚠️
|
|
460
|
+
? "⚠️ Conversation got into a bad state. Use /new to start a fresh session."
|
|
484
461
|
: "Sorry, I'm having trouble right now. Please try again in a few minutes.";
|
|
485
462
|
return {
|
|
486
463
|
kind: "final",
|