@mono-agent/agent-runtime 0.20.3 → 0.20.4
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/package.json
CHANGED
|
@@ -389,7 +389,7 @@ function readSkillTool(skillNames = [], { skillsRoot, dataDir, skills = [] } = {
|
|
|
389
389
|
return {
|
|
390
390
|
name: "ReadSkill",
|
|
391
391
|
label: "Read Skill",
|
|
392
|
-
description: "Load the complete instructions for a named skill. Use ReadSkill instead of Read for SKILL.md files.",
|
|
392
|
+
description: "Load the complete instructions for a named skill. Use ReadSkill instead of Read for SKILL.md files. If a skill's instructions are already present in this conversation, apply those instead of loading it again.",
|
|
393
393
|
parameters: objectSchema({ name: { type: "string", enum: enumNames } }, ["name"]),
|
|
394
394
|
async execute(_toolCallId, { name }) {
|
|
395
395
|
if (sharedRoot) {
|
|
@@ -473,6 +473,14 @@ export function getPiBuiltinTools(allowedTools, {
|
|
|
473
473
|
minimum: 1,
|
|
474
474
|
description: "Exact timeout in milliseconds.",
|
|
475
475
|
};
|
|
476
|
+
// Shared by Exec and Bash, and injected only when the host supplies a
|
|
477
|
+
// process-job controller. House style for a tool description is
|
|
478
|
+
// capability + when-to-prefer + caveat, so the middle sentence is what tells
|
|
479
|
+
// the model which commands belong here rather than in the foreground.
|
|
480
|
+
const backgroundSchema = {
|
|
481
|
+
type: "boolean",
|
|
482
|
+
description: "Run as a durable background process job and notify this conversation when it finishes. Prefer this for work that outlives a reply — builds, full test suites, long installs, migrations, long-running watchers — and leave it off whenever you need the output to answer right now. Do not use for commands that daemonize into another POSIX process group or session.",
|
|
483
|
+
};
|
|
476
484
|
// Per-tool closure config (cwd/event sink/limits/policy) plus the per-instance
|
|
477
485
|
// ToolContext `ctx` that the tool impls and shared helpers read from.
|
|
478
486
|
const toolContext = {
|
|
@@ -533,9 +541,7 @@ export function getPiBuiltinTools(allowedTools, {
|
|
|
533
541
|
timeout_ms: processTimeoutSchema,
|
|
534
542
|
timeout: legacyBashTimeoutSchema,
|
|
535
543
|
max_output_chars: bashLimitSchema,
|
|
536
|
-
...(processJobsController ? {
|
|
537
|
-
background: { type: "boolean", description: "Run as a durable background process job and notify this conversation when it finishes. Do not use for commands that daemonize into another POSIX process group or session." },
|
|
538
|
-
} : {}),
|
|
544
|
+
...(processJobsController ? { background: backgroundSchema } : {}),
|
|
539
545
|
}, ["command"]), bashToolRun, toolContext),
|
|
540
546
|
Exec: createBuiltinTool("Exec", "Exec", "Execute one program directly from an argv array without shell parsing. Prefer this for ordinary commands; use Bash only when shell syntax is required.", objectSchema({
|
|
541
547
|
executable: { type: "string", minLength: 1 },
|
|
@@ -543,9 +549,7 @@ export function getPiBuiltinTools(allowedTools, {
|
|
|
543
549
|
workdir: { type: "string" },
|
|
544
550
|
timeout_ms: processTimeoutSchema,
|
|
545
551
|
max_output_chars: bashLimitSchema,
|
|
546
|
-
...(processJobsController ? {
|
|
547
|
-
background: { type: "boolean", description: "Run as a durable background process job and notify this conversation when it finishes. Do not use for commands that daemonize into another POSIX process group or session." },
|
|
548
|
-
} : {}),
|
|
552
|
+
...(processJobsController ? { background: backgroundSchema } : {}),
|
|
549
553
|
}, ["executable"]), execToolRun, toolContext),
|
|
550
554
|
NodeRepl: nodeReplController
|
|
551
555
|
? createBuiltinTool(
|
|
@@ -84,7 +84,7 @@ export async function handOffProcessJob({
|
|
|
84
84
|
started_at: result.startedAt,
|
|
85
85
|
};
|
|
86
86
|
return {
|
|
87
|
-
text: JSON.stringify(payload)
|
|
87
|
+
text: `${BACKGROUND_START_GUIDANCE}\n${JSON.stringify(payload)}`,
|
|
88
88
|
outcome: {
|
|
89
89
|
status: "ok",
|
|
90
90
|
code: "background_started",
|
|
@@ -111,6 +111,16 @@ export async function handOffProcessJob({
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* A bare id/state payload leaves the model to guess what happens next, and the
|
|
116
|
+
* cheapest wrong guess is a polling loop. Completion delivers its own turn, so
|
|
117
|
+
* the result says so itself rather than relying on the schema line alone. No
|
|
118
|
+
* operator command is named on purpose: the model has a shell, and naming a
|
|
119
|
+
* status command invites exactly the polling this forbids.
|
|
120
|
+
*/
|
|
121
|
+
const BACKGROUND_START_GUIDANCE =
|
|
122
|
+
"Background process job started (tool-authored guidance): this conversation is woken with a new turn when the job reaches a terminal state, and its output arrives with that turn. Do not poll, sleep, wait on it, or re-run the command to check progress, and do not report the work as finished yet.";
|
|
123
|
+
|
|
114
124
|
const PUBLIC_BACKGROUND_START_FAILURES = Object.freeze({
|
|
115
125
|
background_unsupported: "Background process jobs are unsupported for this tool call.",
|
|
116
126
|
background_unsupported_channel: "Background process jobs are unsupported for this channel.",
|
package/src/ai/failure.js
CHANGED
|
@@ -84,7 +84,7 @@ const PROVIDER_AUTH_RE = /(no api key|missing api key|api key required|invalid a
|
|
|
84
84
|
// like worklab's coordinator, independent of retryableProviderFailureInfo) maps
|
|
85
85
|
// that same terse text to the generic "spawn" kind instead of
|
|
86
86
|
// "provider_unavailable".
|
|
87
|
-
const PROVIDER_UNAVAILABLE_RE = /(econn|enotfound|etimedout|timed? ?out|service unavailable|503|502|gateway|fetch failed|network|websocket|\bconnection (?:error|refused|failed)\b|\bcould not connect\b)/i;
|
|
87
|
+
const PROVIDER_UNAVAILABLE_RE = /(econn|enotfound|etimedout|timed? ?out|service unavailable|503|502|gateway|fetch failed|network|websocket|\bconnection (?:error|refused|failed)\b|\bcould not connect\b|\bstream ended without finish_reason\b)/i;
|
|
88
88
|
const TOOL_FAILURE_RE = /(tool .* failed|mcp tool|permission denied|EACCES|read-only file system)/i;
|
|
89
89
|
const NON_RETRYABLE_PROVIDER_RE = /(invalid[_ ]request|unknown parameter|no api key|missing api key|api key required|invalid api key|incorrect api key|provider is not configured:|authentication|authorization|not authorized|forbidden|billing|insufficient[_ ]quota|quota exceeded|model[_ ]not[_ ]found|unsupported model|permission denied|bad request|401|403|404)/i;
|
|
90
90
|
// pi 0.80's openai-client-style bridge collapses a connection-refused/unreachable
|
|
@@ -92,7 +92,15 @@ const NON_RETRYABLE_PROVIDER_RE = /(invalid[_ ]request|unknown parameter|no api
|
|
|
92
92
|
// no fetch failed) — the `\bconnection (?:error|refused|failed)\b|\bcould not connect\b`
|
|
93
93
|
// alternation below is the motivating fix so that case still fails over instead of
|
|
94
94
|
// being classified as non-retryable.
|
|
95
|
-
|
|
95
|
+
// pi-ai 0.83.0 adds a second such bare sentence: openai-completions.js throws
|
|
96
|
+
// "Stream ended without finish_reason" when an SSE stream terminates without a
|
|
97
|
+
// finish_reason chunk. Because pi-models.js pins every custom/OpenAI-compatible
|
|
98
|
+
// provider to the openai-completions api, a truncated gateway response was terminal
|
|
99
|
+
// on the first occurrence — a configured fallback chain never advanced off the
|
|
100
|
+
// primary. Matched as the full sentence on purpose: mono-agent's own TUI/web
|
|
101
|
+
// transports emit a similar "Stream ended without a finish or error frame." that is a
|
|
102
|
+
// local protocol fault and must NOT trigger a provider failover.
|
|
103
|
+
const RETRYABLE_PROVIDER_RE = /(currently overloaded|server(?:s)? (?:is |are )?overloaded|try again later|retry your request|request id|service unavailable|temporar(?:y|ily)|timed? ?out|stream disconnected|fetch failed|econnreset|econnrefused|eai_again|enotfound|etimedout|network|429|too many requests|500|502|503|504|gateway|internal server error|\bconnection (?:error|refused|failed)\b|\bcould not connect\b|\bstream ended without finish_reason\b)/i;
|
|
96
104
|
export const PROVIDER_ABORT_RE = /\b(?:terminated|aborted before final output|aborted before final|stream aborted|stream was aborted|stream disconnected|websocket (?:error|disconnected|closed)|socket hang up|und_err_socket|econnreset|premature close)\b/i;
|
|
97
105
|
|
|
98
106
|
/**
|
|
@@ -129,7 +137,12 @@ function retryableProviderSubkind(text) {
|
|
|
129
137
|
if (/timed? ?out|etimedout/i.test(text)) return "timeout";
|
|
130
138
|
// pi 0.80's terse "Connection error." (no ECONNREFUSED/fetch-failed detail) still
|
|
131
139
|
// needs to land in the "network" subkind so a down provider fails over.
|
|
132
|
-
|
|
140
|
+
// pi-ai 0.83.0's openai-completions adapter throws the bare sentence
|
|
141
|
+
// "Stream ended without finish_reason" when an OpenAI-compatible SSE stream closes
|
|
142
|
+
// cleanly but no chunk ever carried a truthy finish_reason — i.e. a gateway
|
|
143
|
+
// truncated the response. That is a provider-side outage, so it belongs in the same
|
|
144
|
+
// "network" subkind as the terse connection error above.
|
|
145
|
+
if (/stream disconnected|fetch failed|econnreset|econnrefused|eai_again|enotfound|network|\bconnection (?:error|refused|failed)\b|\bcould not connect\b|\bstream ended without finish_reason\b/i.test(text)) return "network";
|
|
133
146
|
if (/500|502|503|504|service unavailable|gateway|internal server error/i.test(text)) return "server_error";
|
|
134
147
|
if (/retry your request|try again later|request id|processing your request/i.test(text)) return "retryable_request";
|
|
135
148
|
return null;
|