@poncho-ai/harness 0.59.12 → 0.59.14
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/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +34 -0
- package/dist/index.js +104 -32
- package/package.json +1 -1
- package/src/ask-user-tool.ts +95 -0
- package/src/default-agent.ts +1 -1
- package/src/harness.ts +9 -0
- package/src/memory.ts +36 -22
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.59.
|
|
2
|
+
> @poncho-ai/harness@0.59.14 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
|
|
3
3
|
> node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[embed-docs] Generated poncho-docs.ts with 4 topics
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
[34mCLI[39m Target: es2022
|
|
10
10
|
[34mESM[39m Build start
|
|
11
11
|
[32mESM[39m [1mdist/isolate-F2PPSUL6.js [22m[32m53.82 KB[39m
|
|
12
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
13
|
-
[32mESM[39m ⚡️ Build success in
|
|
12
|
+
[32mESM[39m [1mdist/index.js [22m[32m564.58 KB[39m
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 217ms
|
|
14
14
|
[34mDTS[39m Build start
|
|
15
|
-
[32mDTS[39m ⚡️ Build success in
|
|
15
|
+
[32mDTS[39m ⚡️ Build success in 8055ms
|
|
16
16
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m102.50 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.59.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#172](https://github.com/cesr/poncho-ai/pull/172) [`dc61836`](https://github.com/cesr/poncho-ai/commit/dc61836aa99e3cb6e1339dc6f82ffdab522918ba) Thanks [@cesr](https://github.com/cesr)! - Add the `ask_user` built-in tool: the agent can pause the run to ask the
|
|
8
|
+
user a structured, multiple-choice question (the in-app analog of Claude
|
|
9
|
+
Code's AskUserQuestion) instead of asking in plain prose. Each call
|
|
10
|
+
carries 1–4 questions, each with a short header, a `multiSelect` flag,
|
|
11
|
+
and pre-made options; a free-text "Other" escape is rendered by the
|
|
12
|
+
client.
|
|
13
|
+
|
|
14
|
+
The tool is forced to client (`device`) dispatch, so the harness pauses
|
|
15
|
+
the run on a checkpoint carrying the questions and the consumer resumes
|
|
16
|
+
by injecting the user's selections as the tool result — no server-side
|
|
17
|
+
execution (the handler is a defensive stub). The default agent prompt
|
|
18
|
+
now steers the model to reach for `ask_user` whenever it would otherwise
|
|
19
|
+
stop to ask the user to choose between options.
|
|
20
|
+
|
|
21
|
+
## 0.59.13
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- [`bf77523`](https://github.com/cesr/poncho-ai/commit/bf775237af883a53ee90f0a620434a1929ac82ea) Thanks [@cesr](https://github.com/cesr)! - `memory_main_edit` now treats an empty `old_str` as append: `new_str` is
|
|
26
|
+
appended to the end of memory (separated by a blank line when memory is
|
|
27
|
+
non-empty). This also handles the first-ever write into empty memory, so
|
|
28
|
+
`memory_main_edit` alone can bootstrap, add new facts, and edit existing
|
|
29
|
+
text — consumers that want to drop `memory_main_write` from the tool
|
|
30
|
+
surface no longer lose the ability to seed empty memory.
|
|
31
|
+
|
|
32
|
+
Both `memory_main_edit` and `memory_main_write` now return a minimal
|
|
33
|
+
`{ ok: true, bytes }` result instead of echoing the entire memory
|
|
34
|
+
document back as the tool result, which kept re-injecting the whole
|
|
35
|
+
document into the conversation on every targeted edit.
|
|
36
|
+
|
|
3
37
|
## 0.59.12
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
package/dist/index.js
CHANGED
|
@@ -710,7 +710,7 @@ Environment: {{runtime.environment}}
|
|
|
710
710
|
|
|
711
711
|
- Use tools when needed
|
|
712
712
|
- Explain your reasoning clearly
|
|
713
|
-
-
|
|
713
|
+
- When requirements are ambiguous or you need the user to choose between options, use the \`ask_user\` tool to ask a structured multiple-choice question instead of writing the question as plain text. Reserve plain-text questions for genuinely open-ended asks that have no sensible pre-made options.
|
|
714
714
|
- Never claim a file/tool change unless the corresponding tool call actually succeeded
|
|
715
715
|
`;
|
|
716
716
|
};
|
|
@@ -1642,7 +1642,7 @@ When \`memory.enabled\` is true in \`poncho.config.js\`, the harness enables a s
|
|
|
1642
1642
|
|
|
1643
1643
|
- A single persistent main memory document is loaded at run start and interpolated into the system prompt under \`## Persistent Memory\`.
|
|
1644
1644
|
- \`memory_main_write\` overwrites the entire memory document (for initial writes or full rewrites).
|
|
1645
|
-
- \`memory_main_edit\` performs targeted string-replacement edits on memory (find \`old_str\`, replace with \`new_str\`), mirroring \`edit_file\` semantics. The tool description instructs the model to proactively evaluate each turn whether durable memory should be updated.
|
|
1645
|
+
- \`memory_main_edit\` performs targeted string-replacement edits on memory (find \`old_str\`, replace with \`new_str\`), mirroring \`edit_file\` semantics. An empty \`old_str\` appends \`new_str\` to the end of memory, which also handles the first-ever write into empty memory. The tool description instructs the model to proactively evaluate each turn whether durable memory should be updated.
|
|
1646
1646
|
- \`conversation_recall\` can search, browse, and fetch past conversations. It supports keyword search (scoring by relevance), date-range filtering (\`after\`/\`before\`), and fetching a specific conversation's full message history by ID.
|
|
1647
1647
|
|
|
1648
1648
|
\`\`\`javascript
|
|
@@ -2445,7 +2445,7 @@ var ponchoDocsTool = defineTool({
|
|
|
2445
2445
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
2446
2446
|
import { readFile as readFile9 } from "fs/promises";
|
|
2447
2447
|
import { resolve as resolve11 } from "path";
|
|
2448
|
-
import { defineTool as
|
|
2448
|
+
import { defineTool as defineTool13, getTextContent as getTextContent2, createLogger as createLogger7, formatError as fmtErr, url as urlColor } from "@poncho-ai/sdk";
|
|
2449
2449
|
|
|
2450
2450
|
// src/upload-store.ts
|
|
2451
2451
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -5970,22 +5970,22 @@ var createMemoryTools = (store, options) => {
|
|
|
5970
5970
|
throw new Error("content is required");
|
|
5971
5971
|
}
|
|
5972
5972
|
const memory = await resolveStore(context).updateMainMemory({ content });
|
|
5973
|
-
return { ok: true, memory };
|
|
5973
|
+
return { ok: true, bytes: memory.content.length };
|
|
5974
5974
|
}
|
|
5975
5975
|
}),
|
|
5976
5976
|
defineTool6({
|
|
5977
5977
|
name: "memory_main_edit",
|
|
5978
|
-
description: "Edit persistent main memory
|
|
5978
|
+
description: "Edit persistent main memory. With a non-empty old_str, replace that exact string (which must match exactly one location) with new_str; use an empty new_str to delete the matched content. With an empty old_str, append new_str to the end of memory \u2014 use this to add a brand-new fact or to write the first fact when memory is still empty. Proactively evaluate every turn whether memory should be updated.",
|
|
5979
5979
|
inputSchema: {
|
|
5980
5980
|
type: "object",
|
|
5981
5981
|
properties: {
|
|
5982
5982
|
old_str: {
|
|
5983
5983
|
type: "string",
|
|
5984
|
-
description: "The exact text to find and replace (must be unique in memory). Include surrounding context if needed to ensure uniqueness."
|
|
5984
|
+
description: "The exact text to find and replace (must be unique in memory). Include surrounding context if needed to ensure uniqueness. Leave empty to append new_str to the end of memory instead."
|
|
5985
5985
|
},
|
|
5986
5986
|
new_str: {
|
|
5987
5987
|
type: "string",
|
|
5988
|
-
description: "The replacement text (use empty string to delete the matched content)"
|
|
5988
|
+
description: "The replacement text (use empty string to delete the matched content), or the text to append when old_str is empty."
|
|
5989
5989
|
}
|
|
5990
5990
|
},
|
|
5991
5991
|
required: ["old_str", "new_str"],
|
|
@@ -5994,26 +5994,33 @@ var createMemoryTools = (store, options) => {
|
|
|
5994
5994
|
handler: async (input, context) => {
|
|
5995
5995
|
const oldStr = typeof input.old_str === "string" ? input.old_str : "";
|
|
5996
5996
|
const newStr = typeof input.new_str === "string" ? input.new_str : "";
|
|
5997
|
-
if (!oldStr) {
|
|
5998
|
-
throw new Error("old_str must not be empty.");
|
|
5999
|
-
}
|
|
6000
5997
|
const current = await resolveStore(context).getMainMemory();
|
|
6001
5998
|
const content = current.content;
|
|
6002
|
-
|
|
6003
|
-
if (
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
)
|
|
5999
|
+
let newContent;
|
|
6000
|
+
if (!oldStr) {
|
|
6001
|
+
if (!newStr) {
|
|
6002
|
+
throw new Error("new_str must not be empty when appending (old_str is empty).");
|
|
6003
|
+
}
|
|
6004
|
+
newContent = content ? `${content.replace(/\s+$/, "")}
|
|
6005
|
+
|
|
6006
|
+
${newStr}` : newStr;
|
|
6007
|
+
} else {
|
|
6008
|
+
const first = content.indexOf(oldStr);
|
|
6009
|
+
if (first === -1) {
|
|
6010
|
+
throw new Error(
|
|
6011
|
+
"old_str not found in memory. Make sure it matches exactly, including whitespace and line breaks."
|
|
6012
|
+
);
|
|
6013
|
+
}
|
|
6014
|
+
const last = content.lastIndexOf(oldStr);
|
|
6015
|
+
if (first !== last) {
|
|
6016
|
+
throw new Error(
|
|
6017
|
+
"old_str appears multiple times in memory. Please provide more context to ensure a unique match."
|
|
6018
|
+
);
|
|
6019
|
+
}
|
|
6020
|
+
newContent = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
|
|
6013
6021
|
}
|
|
6014
|
-
const newContent = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
|
|
6015
6022
|
const memory = await resolveStore(context).updateMainMemory({ content: newContent });
|
|
6016
|
-
return { ok: true, memory };
|
|
6023
|
+
return { ok: true, bytes: memory.content.length };
|
|
6017
6024
|
}
|
|
6018
6025
|
}),
|
|
6019
6026
|
defineTool6({
|
|
@@ -8584,10 +8591,71 @@ var createSearchTools = () => [
|
|
|
8584
8591
|
})
|
|
8585
8592
|
];
|
|
8586
8593
|
|
|
8587
|
-
// src/
|
|
8594
|
+
// src/ask-user-tool.ts
|
|
8588
8595
|
import { defineTool as defineTool11 } from "@poncho-ai/sdk";
|
|
8596
|
+
var createAskUserTool = () => defineTool11({
|
|
8597
|
+
name: "ask_user",
|
|
8598
|
+
description: "Ask the user one or more structured multiple-choice questions and wait for their answer. Use this INSTEAD of writing a question as plain text whenever you would otherwise pause to let the user choose between options \u2014 clarifying ambiguous requirements, picking an approach, confirming a direction. The user sees tappable option chips and answers with a tap. Prefer this over a prose question: it is faster for the user and gives you a clean answer. Guidelines: ask 1\u20134 questions at once, each with 2\u20134 concrete options; keep `header` very short (a few words); write each option `label` short and its `description` to one line. The user can always type a custom 'Other' answer, so you do not need to add one. Do NOT call any other tool in the same turn as ask_user, and call it at most once per turn. After the user answers you will receive their selections and may continue.",
|
|
8599
|
+
inputSchema: {
|
|
8600
|
+
type: "object",
|
|
8601
|
+
properties: {
|
|
8602
|
+
questions: {
|
|
8603
|
+
type: "array",
|
|
8604
|
+
description: "1\u20134 questions to ask the user at once.",
|
|
8605
|
+
items: {
|
|
8606
|
+
type: "object",
|
|
8607
|
+
properties: {
|
|
8608
|
+
question: {
|
|
8609
|
+
type: "string",
|
|
8610
|
+
description: "The full question text shown to the user."
|
|
8611
|
+
},
|
|
8612
|
+
header: {
|
|
8613
|
+
type: "string",
|
|
8614
|
+
description: "A very short label for this question (a few words, ~12 chars), shown as a chip."
|
|
8615
|
+
},
|
|
8616
|
+
multiSelect: {
|
|
8617
|
+
type: "boolean",
|
|
8618
|
+
description: "If true, the user may select multiple options. Defaults to false (single choice)."
|
|
8619
|
+
},
|
|
8620
|
+
options: {
|
|
8621
|
+
type: "array",
|
|
8622
|
+
description: "The pre-made options. A free-text 'Other' option is added automatically by the client.",
|
|
8623
|
+
items: {
|
|
8624
|
+
type: "object",
|
|
8625
|
+
properties: {
|
|
8626
|
+
label: {
|
|
8627
|
+
type: "string",
|
|
8628
|
+
description: "Short, selectable label for this option."
|
|
8629
|
+
},
|
|
8630
|
+
description: {
|
|
8631
|
+
type: "string",
|
|
8632
|
+
description: "A one-line explanation of what this option means."
|
|
8633
|
+
}
|
|
8634
|
+
},
|
|
8635
|
+
required: ["label"],
|
|
8636
|
+
additionalProperties: false
|
|
8637
|
+
}
|
|
8638
|
+
}
|
|
8639
|
+
},
|
|
8640
|
+
required: ["question", "header", "options"],
|
|
8641
|
+
additionalProperties: false
|
|
8642
|
+
}
|
|
8643
|
+
}
|
|
8644
|
+
},
|
|
8645
|
+
required: ["questions"],
|
|
8646
|
+
additionalProperties: false
|
|
8647
|
+
},
|
|
8648
|
+
handler: async () => {
|
|
8649
|
+
return {
|
|
8650
|
+
error: "ask_user must be answered by the user on the client; it cannot run server-side. This indicates a dispatch misconfiguration."
|
|
8651
|
+
};
|
|
8652
|
+
}
|
|
8653
|
+
});
|
|
8654
|
+
|
|
8655
|
+
// src/subagent-tools.ts
|
|
8656
|
+
import { defineTool as defineTool12 } from "@poncho-ai/sdk";
|
|
8589
8657
|
var createSubagentTools = (manager) => [
|
|
8590
|
-
|
|
8658
|
+
defineTool12({
|
|
8591
8659
|
name: "spawn_subagent",
|
|
8592
8660
|
description: "Spawn a subagent to work on a task in the background. Returns immediately with a subagent ID. The subagent runs independently and its result will be delivered to you as a message in the conversation when it completes.\n\nGuidelines:\n- Spawn all needed subagents in a SINGLE response (they run concurrently), then end your turn with a brief message to the user.\n- Do NOT spawn more subagents in follow-up steps. Wait for results to be delivered before deciding if more work is needed.\n- Prefer doing work yourself for simple or quick tasks. Spawn subagents for substantial, self-contained work.\n- The subagent has no memory of your conversation -- write thorough, self-contained instructions in the task.",
|
|
8593
8661
|
inputSchema: {
|
|
@@ -8622,7 +8690,7 @@ var createSubagentTools = (manager) => [
|
|
|
8622
8690
|
return { subagentId, status: "running" };
|
|
8623
8691
|
}
|
|
8624
8692
|
}),
|
|
8625
|
-
|
|
8693
|
+
defineTool12({
|
|
8626
8694
|
name: "message_subagent",
|
|
8627
8695
|
description: "Send a follow-up message to a completed or stopped subagent. The subagent restarts in the background and its result will be delivered to you as a message when it completes. Only works when the subagent is not currently running.",
|
|
8628
8696
|
inputSchema: {
|
|
@@ -8650,7 +8718,7 @@ var createSubagentTools = (manager) => [
|
|
|
8650
8718
|
return { subagentId: id, status: "running" };
|
|
8651
8719
|
}
|
|
8652
8720
|
}),
|
|
8653
|
-
|
|
8721
|
+
defineTool12({
|
|
8654
8722
|
name: "stop_subagent",
|
|
8655
8723
|
description: "Stop a running subagent. The subagent's conversation is preserved but it will stop processing. Use this to cancel work that is no longer needed.",
|
|
8656
8724
|
inputSchema: {
|
|
@@ -8673,7 +8741,7 @@ var createSubagentTools = (manager) => [
|
|
|
8673
8741
|
return { message: `Subagent "${subagentId}" has been stopped.` };
|
|
8674
8742
|
}
|
|
8675
8743
|
}),
|
|
8676
|
-
|
|
8744
|
+
defineTool12({
|
|
8677
8745
|
name: "list_subagents",
|
|
8678
8746
|
description: "List all subagents that have been spawned in this conversation. Returns each subagent's ID, original task, current status, and message count. Use this to look up subagent IDs before calling message_subagent or stop_subagent.",
|
|
8679
8747
|
inputSchema: {
|
|
@@ -8693,7 +8761,7 @@ var createSubagentTools = (manager) => [
|
|
|
8693
8761
|
return { subagents };
|
|
8694
8762
|
}
|
|
8695
8763
|
}),
|
|
8696
|
-
|
|
8764
|
+
defineTool12({
|
|
8697
8765
|
name: "read_subagent",
|
|
8698
8766
|
description: "Fetch the conversation transcript of a subagent you spawned. Use this to inspect a subagent's intermediate reasoning, tool calls, or full output -- instead of asking it to repeat its work via message_subagent.\n\nModes:\n- 'final' (default): just the last assistant message. Cheap.\n- 'assistant': all assistant messages, no tool calls/results.\n- 'full': every message including tool calls and results. Can be large.\n\nUse since_index / max_messages to page through long transcripts. Only works on subagents directly spawned by this conversation.",
|
|
8699
8767
|
inputSchema: {
|
|
@@ -9571,6 +9639,7 @@ var AgentHarness = class _AgentHarness {
|
|
|
9571
9639
|
}
|
|
9572
9640
|
/** Returns the normalized {access, dispatch} mode for the tool. */
|
|
9573
9641
|
resolveToolMode(toolName) {
|
|
9642
|
+
if (toolName === "ask_user") return { dispatch: "device" };
|
|
9574
9643
|
return normalizeToolAccess(this.resolveToolAccess(toolName));
|
|
9575
9644
|
}
|
|
9576
9645
|
isToolEnabled(name) {
|
|
@@ -9612,6 +9681,9 @@ var AgentHarness = class _AgentHarness {
|
|
|
9612
9681
|
this.registerIfMissing(tool);
|
|
9613
9682
|
}
|
|
9614
9683
|
}
|
|
9684
|
+
if (this.isToolEnabled("ask_user")) {
|
|
9685
|
+
this.registerIfMissing(createAskUserTool());
|
|
9686
|
+
}
|
|
9615
9687
|
if (this.environment === "development" && this.isToolEnabled("poncho_docs")) {
|
|
9616
9688
|
this.registerIfMissing(ponchoDocsTool);
|
|
9617
9689
|
}
|
|
@@ -9620,7 +9692,7 @@ var AgentHarness = class _AgentHarness {
|
|
|
9620
9692
|
}
|
|
9621
9693
|
}
|
|
9622
9694
|
createGetToolResultByIdTool() {
|
|
9623
|
-
return
|
|
9695
|
+
return defineTool13({
|
|
9624
9696
|
name: "get_tool_result_by_id",
|
|
9625
9697
|
description: "Retrieve a previously archived full tool result by id for the current conversation. Use this when older tool outputs were truncated in prompt history.",
|
|
9626
9698
|
inputSchema: {
|
|
@@ -14647,7 +14719,7 @@ ${draft.toolTimeline.join("\n")}`);
|
|
|
14647
14719
|
};
|
|
14648
14720
|
|
|
14649
14721
|
// src/index.ts
|
|
14650
|
-
import { defineTool as
|
|
14722
|
+
import { defineTool as defineTool14 } from "@poncho-ai/sdk";
|
|
14651
14723
|
export {
|
|
14652
14724
|
AgentHarness,
|
|
14653
14725
|
AgentOrchestrator,
|
|
@@ -14721,7 +14793,7 @@ export {
|
|
|
14721
14793
|
createWriteTool,
|
|
14722
14794
|
decodeFileInputData,
|
|
14723
14795
|
defaultAgentDefinition,
|
|
14724
|
-
|
|
14796
|
+
defineTool14 as defineTool,
|
|
14725
14797
|
deleteOpenAICodexSession,
|
|
14726
14798
|
deriveUploadKey,
|
|
14727
14799
|
ensureAgentIdentity,
|
package/package.json
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { defineTool, type ToolDefinition } from "@poncho-ai/sdk";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// ask_user — pause the run to ask the user a structured, multiple-choice
|
|
5
|
+
// question with pre-made options (the in-app analog of Claude Code's
|
|
6
|
+
// AskUserQuestion). The client renders tappable option chips so the user
|
|
7
|
+
// answers with a tap instead of typing prose.
|
|
8
|
+
//
|
|
9
|
+
// This tool is dispatched to the client ("device" dispatch, forced in
|
|
10
|
+
// AgentHarness.resolveToolMode): the harness pauses the run, emits a
|
|
11
|
+
// checkpoint carrying the questions payload, and the consumer (PonchOS)
|
|
12
|
+
// resumes the run by POSTing the user's selections back as this tool's
|
|
13
|
+
// result. The handler below is a defensive stub — device dispatch
|
|
14
|
+
// intercepts the call before any server-side execution, so it must never
|
|
15
|
+
// actually run.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export const createAskUserTool = (): ToolDefinition =>
|
|
19
|
+
defineTool({
|
|
20
|
+
name: "ask_user",
|
|
21
|
+
description:
|
|
22
|
+
"Ask the user one or more structured multiple-choice questions and wait for their answer. " +
|
|
23
|
+
"Use this INSTEAD of writing a question as plain text whenever you would otherwise pause to " +
|
|
24
|
+
"let the user choose between options — clarifying ambiguous requirements, picking an approach, " +
|
|
25
|
+
"confirming a direction. The user sees tappable option chips and answers with a tap. " +
|
|
26
|
+
"Prefer this over a prose question: it is faster for the user and gives you a clean answer. " +
|
|
27
|
+
"Guidelines: ask 1–4 questions at once, each with 2–4 concrete options; keep `header` very short " +
|
|
28
|
+
"(a few words); write each option `label` short and its `description` to one line. The user can " +
|
|
29
|
+
"always type a custom 'Other' answer, so you do not need to add one. Do NOT call any other tool " +
|
|
30
|
+
"in the same turn as ask_user, and call it at most once per turn. After the user answers you will " +
|
|
31
|
+
"receive their selections and may continue.",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
questions: {
|
|
36
|
+
type: "array",
|
|
37
|
+
description: "1–4 questions to ask the user at once.",
|
|
38
|
+
items: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
question: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "The full question text shown to the user.",
|
|
44
|
+
},
|
|
45
|
+
header: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description:
|
|
48
|
+
"A very short label for this question (a few words, ~12 chars), shown as a chip.",
|
|
49
|
+
},
|
|
50
|
+
multiSelect: {
|
|
51
|
+
type: "boolean",
|
|
52
|
+
description:
|
|
53
|
+
"If true, the user may select multiple options. Defaults to false (single choice).",
|
|
54
|
+
},
|
|
55
|
+
options: {
|
|
56
|
+
type: "array",
|
|
57
|
+
description:
|
|
58
|
+
"The pre-made options. A free-text 'Other' option is added automatically by the client.",
|
|
59
|
+
items: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
label: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Short, selectable label for this option.",
|
|
65
|
+
},
|
|
66
|
+
description: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "A one-line explanation of what this option means.",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
required: ["label"],
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required: ["question", "header", "options"],
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
required: ["questions"],
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
},
|
|
84
|
+
handler: async () => {
|
|
85
|
+
// Unreachable in normal operation: ask_user is forced to client/device
|
|
86
|
+
// dispatch, so the harness checkpoints before this handler is invoked.
|
|
87
|
+
// If it ever runs, the tool was misconfigured (dispatch not forced) —
|
|
88
|
+
// surface an error rather than silently resolving with no user input.
|
|
89
|
+
return {
|
|
90
|
+
error:
|
|
91
|
+
"ask_user must be answered by the user on the client; it cannot run server-side. " +
|
|
92
|
+
"This indicates a dispatch misconfiguration.",
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
});
|
package/src/default-agent.ts
CHANGED
|
@@ -100,7 +100,7 @@ Environment: {{runtime.environment}}
|
|
|
100
100
|
|
|
101
101
|
- Use tools when needed
|
|
102
102
|
- Explain your reasoning clearly
|
|
103
|
-
-
|
|
103
|
+
- When requirements are ambiguous or you need the user to choose between options, use the \`ask_user\` tool to ask a structured multiple-choice question instead of writing the question as plain text. Reserve plain-text questions for genuinely open-ended asks that have no sensible pre-made options.
|
|
104
104
|
- Never claim a file/tool change unless the corresponding tool call actually succeeded
|
|
105
105
|
`;
|
|
106
106
|
};
|
package/src/harness.ts
CHANGED
|
@@ -65,6 +65,7 @@ import { jsonSchemaToZod } from "./schema-converter.js";
|
|
|
65
65
|
import type { SkillMetadata } from "./skill-context.js";
|
|
66
66
|
import { createSkillTools, normalizeScriptPolicyPath } from "./skill-tools.js";
|
|
67
67
|
import { createSearchTools } from "./search-tools.js";
|
|
68
|
+
import { createAskUserTool } from "./ask-user-tool.js";
|
|
68
69
|
import { createSubagentTools } from "./subagent-tools.js";
|
|
69
70
|
import type { SubagentManager } from "./subagent-manager.js";
|
|
70
71
|
import { trace, context as otelContext, createContextKey, type Context as OtelContextType, SpanStatusCode, SpanKind, diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
|
|
@@ -965,6 +966,11 @@ export class AgentHarness {
|
|
|
965
966
|
|
|
966
967
|
/** Returns the normalized {access, dispatch} mode for the tool. */
|
|
967
968
|
private resolveToolMode(toolName: string): { access?: "approval"; dispatch?: "device" } {
|
|
969
|
+
// ask_user is always answered by the user on the client. Force device
|
|
970
|
+
// dispatch unconditionally so it pauses the run and checkpoints rather
|
|
971
|
+
// than running its (defensive, error-returning) server-side handler —
|
|
972
|
+
// even if no `poncho.config.js` entry exists for it.
|
|
973
|
+
if (toolName === "ask_user") return { dispatch: "device" };
|
|
968
974
|
return normalizeToolAccess(this.resolveToolAccess(toolName));
|
|
969
975
|
}
|
|
970
976
|
|
|
@@ -1014,6 +1020,9 @@ export class AgentHarness {
|
|
|
1014
1020
|
this.registerIfMissing(tool);
|
|
1015
1021
|
}
|
|
1016
1022
|
}
|
|
1023
|
+
if (this.isToolEnabled("ask_user")) {
|
|
1024
|
+
this.registerIfMissing(createAskUserTool());
|
|
1025
|
+
}
|
|
1017
1026
|
if (this.environment === "development" && this.isToolEnabled("poncho_docs")) {
|
|
1018
1027
|
this.registerIfMissing(ponchoDocsTool);
|
|
1019
1028
|
}
|
package/src/memory.ts
CHANGED
|
@@ -192,15 +192,17 @@ export const createMemoryTools = (
|
|
|
192
192
|
throw new Error("content is required");
|
|
193
193
|
}
|
|
194
194
|
const memory = await resolveStore(context).updateMainMemory({ content });
|
|
195
|
-
return { ok: true, memory };
|
|
195
|
+
return { ok: true, bytes: memory.content.length };
|
|
196
196
|
},
|
|
197
197
|
}),
|
|
198
198
|
defineTool({
|
|
199
199
|
name: "memory_main_edit",
|
|
200
200
|
description:
|
|
201
|
-
"Edit persistent main memory
|
|
202
|
-
"
|
|
203
|
-
"
|
|
201
|
+
"Edit persistent main memory. With a non-empty old_str, replace that exact " +
|
|
202
|
+
"string (which must match exactly one location) with new_str; use an empty " +
|
|
203
|
+
"new_str to delete the matched content. With an empty old_str, append new_str " +
|
|
204
|
+
"to the end of memory — use this to add a brand-new fact or to write the first " +
|
|
205
|
+
"fact when memory is still empty. " +
|
|
204
206
|
"Proactively evaluate every turn whether memory should be updated.",
|
|
205
207
|
inputSchema: {
|
|
206
208
|
type: "object",
|
|
@@ -209,11 +211,14 @@ export const createMemoryTools = (
|
|
|
209
211
|
type: "string",
|
|
210
212
|
description:
|
|
211
213
|
"The exact text to find and replace (must be unique in memory). " +
|
|
212
|
-
"Include surrounding context if needed to ensure uniqueness."
|
|
214
|
+
"Include surrounding context if needed to ensure uniqueness. " +
|
|
215
|
+
"Leave empty to append new_str to the end of memory instead.",
|
|
213
216
|
},
|
|
214
217
|
new_str: {
|
|
215
218
|
type: "string",
|
|
216
|
-
description:
|
|
219
|
+
description:
|
|
220
|
+
"The replacement text (use empty string to delete the matched content), " +
|
|
221
|
+
"or the text to append when old_str is empty.",
|
|
217
222
|
},
|
|
218
223
|
},
|
|
219
224
|
required: ["old_str", "new_str"],
|
|
@@ -222,26 +227,35 @@ export const createMemoryTools = (
|
|
|
222
227
|
handler: async (input, context) => {
|
|
223
228
|
const oldStr = typeof input.old_str === "string" ? input.old_str : "";
|
|
224
229
|
const newStr = typeof input.new_str === "string" ? input.new_str : "";
|
|
225
|
-
if (!oldStr) {
|
|
226
|
-
throw new Error("old_str must not be empty.");
|
|
227
|
-
}
|
|
228
230
|
const current = await resolveStore(context).getMainMemory();
|
|
229
231
|
const content = current.content;
|
|
230
|
-
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
232
|
+
let newContent: string;
|
|
233
|
+
if (!oldStr) {
|
|
234
|
+
// Append mode: add new_str to the end. Handles the first-ever write
|
|
235
|
+
// (empty memory) and adding new facts without needing existing text
|
|
236
|
+
// to match. Separate from prior content with a blank line when both
|
|
237
|
+
// sides are non-empty.
|
|
238
|
+
if (!newStr) {
|
|
239
|
+
throw new Error("new_str must not be empty when appending (old_str is empty).");
|
|
240
|
+
}
|
|
241
|
+
newContent = content ? `${content.replace(/\s+$/, "")}\n\n${newStr}` : newStr;
|
|
242
|
+
} else {
|
|
243
|
+
const first = content.indexOf(oldStr);
|
|
244
|
+
if (first === -1) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
"old_str not found in memory. Make sure it matches exactly, including whitespace and line breaks.",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const last = content.lastIndexOf(oldStr);
|
|
250
|
+
if (first !== last) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
"old_str appears multiple times in memory. Please provide more context to ensure a unique match.",
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
newContent = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
|
|
241
256
|
}
|
|
242
|
-
const newContent = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
|
|
243
257
|
const memory = await resolveStore(context).updateMainMemory({ content: newContent });
|
|
244
|
-
return { ok: true, memory };
|
|
258
|
+
return { ok: true, bytes: memory.content.length };
|
|
245
259
|
},
|
|
246
260
|
}),
|
|
247
261
|
defineTool({
|