mixdog 1.3.0 → 1.3.1
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/NOTICE.md +52 -9
- package/package.json +1 -1
- package/src/defaults/shipped-history.json +4 -1
- package/src/defaults/skills/setup/references/actions.md +4 -1
- package/src/defaults/skills/setup-guide/SKILL.md +10 -7
- package/src/output-styles/common.md +13 -8
- package/src/rules/agent-mode/AGENT_MODE.md +1 -1
- package/src/rules/agent-mode/MIXY.md +11 -4
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +72 -14
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +4 -2
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +14 -4
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +37 -7
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +4 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +17 -8
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +32 -5
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +8 -14
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +46 -4
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +7 -20
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +17 -1
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +61 -9
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +14 -9
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
- package/src/runtime/attachments/store.mjs +6 -0
- package/src/runtime/effort-judge/judge-client.mjs +1 -1
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/shared/agent-kind.mjs +152 -0
- package/src/runtime/shared/agent-route-config.d.mts +1 -0
- package/src/runtime/shared/agent-route-config.mjs +22 -8
- package/src/runtime/shared/agent-store.mjs +132 -0
- package/src/runtime/shared/bot-split-migration.mjs +457 -0
- package/src/runtime/shared/config-patch.mjs +20 -1
- package/src/runtime/shared/connector-catalog.d.mts +1 -2
- package/src/runtime/shared/connector-catalog.mjs +2 -4
- package/src/runtime/shared/connector-triggers.mjs +2 -0
- package/src/runtime/shared/err-text.mjs +3 -1
- package/src/runtime/shared/error-presentation.mjs +91 -6
- package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
- package/src/runtime/shared/handoff-artifacts.mjs +61 -0
- package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
- package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
- package/src/runtime/shared/model-route-fallback.mjs +79 -7
- package/src/runtime/shared/plain-preview.mjs +22 -17
- package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
- package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
- package/src/runtime/shared/usage-readout-totals.mjs +15 -13
- package/src/runtime/shared/user-data-guard.mjs +5 -2
- package/src/session-runtime/agent-home-delivery.mjs +423 -56
- package/src/session-runtime/agent-room-delivery.mjs +45 -4
- package/src/session-runtime/agent-task-sessions.mjs +299 -39
- package/src/session-runtime/boot/apis.mjs +13 -0
- package/src/session-runtime/boot/providers.mjs +14 -4
- package/src/session-runtime/config-lifecycle.mjs +43 -2
- package/src/session-runtime/handoff-attachments.mjs +593 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +27 -3
- package/src/session-runtime/model-route/route-persist.mjs +2 -2
- package/src/session-runtime/model-route/route-tuning.mjs +7 -1
- package/src/session-runtime/model-route/set-route.mjs +21 -1
- package/src/session-runtime/native-web-search.mjs +2 -2
- package/src/session-runtime/new-session-config.mjs +6 -1
- package/src/session-runtime/route-resolution.mjs +5 -11
- package/src/session-runtime/services/agent-tool/helpers.mjs +24 -5
- package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
- package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
- package/src/session-runtime/setup-tool/executor.mjs +12 -1
- package/src/session-runtime/setup-tool/extended-actions.mjs +19 -7
- package/src/session-runtime/setup-tool/settings-contract.mjs +6 -0
- package/src/session-runtime/tool-policy-refresh.mjs +13 -2
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +11 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +123 -32
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
- package/src/standalone/daemon-session-control.mjs +13 -5
- package/src/standalone/daemon.mjs +16 -4
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
- package/src/standalone/session-service/agent-tree.mjs +4 -3
- package/src/standalone/session-service/session-calls/session-turns.mjs +19 -0
- package/src/tui/dist/index.mjs +70 -9
- package/src/tui/session/notice-surface.mjs +3 -4
- package/src/tui/session/session-api/routes.mjs +14 -3
- package/src/tui/session/transcript-intake.mjs +2 -0
- package/src/tui/session/turn-live-call.mjs +11 -3
- package/src/tui/session/turn.mjs +13 -3
|
@@ -335,8 +335,19 @@ const CODE_KINDS = new Map(
|
|
|
335
335
|
quota_exhausted: ['quota_exceeded', 'quota_exhausted', 'usage_limit_reached', 'provider_accounts_exhausted'],
|
|
336
336
|
context_overflow: ['context_length_exceeded', 'context_window_exceeded', 'model_context_window_exceeded'],
|
|
337
337
|
request_too_large: ['request_too_large', 'payload_too_large', 'request_entity_too_large'],
|
|
338
|
-
|
|
339
|
-
|
|
338
|
+
// Google names a rejected or expired key with an ErrorInfo reason on an
|
|
339
|
+
// INVALID_ARGUMENT 400; the reason outranks the envelope status.
|
|
340
|
+
auth: [
|
|
341
|
+
'invalid_api_key',
|
|
342
|
+
'authentication_error',
|
|
343
|
+
'unauthenticated',
|
|
344
|
+
'unauthorized',
|
|
345
|
+
'invalid_authentication',
|
|
346
|
+
'api_key_invalid',
|
|
347
|
+
'api_key_expired',
|
|
348
|
+
'access_token_expired',
|
|
349
|
+
],
|
|
350
|
+
permission: ['permission_error', 'permission_denied', 'forbidden', 'service_disabled', 'api_key_service_blocked'],
|
|
340
351
|
overloaded: [
|
|
341
352
|
'server_is_overloaded',
|
|
342
353
|
'overloaded_error',
|
|
@@ -374,6 +385,7 @@ const CODE_KINDS = new Map(
|
|
|
374
385
|
'not_found',
|
|
375
386
|
'not_found_error',
|
|
376
387
|
'model_not_found',
|
|
388
|
+
'model_not_supported',
|
|
377
389
|
'unsupported_parameter',
|
|
378
390
|
],
|
|
379
391
|
}).flatMap(([kind, codes]) => codes.map((code) => [code, kind]))
|
|
@@ -150,6 +150,13 @@ function isOverloadFailure(status, errorKind) {
|
|
|
150
150
|
return status === 529 || errorKind === 'overloaded';
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
function exceedsAdviceCeiling(caught, opts) {
|
|
154
|
+
const ceiling = Number(opts.maxRetryAfterMs);
|
|
155
|
+
if (!(ceiling > 0)) return false;
|
|
156
|
+
const advised = retryAfterMsFromError(caught);
|
|
157
|
+
return advised != null && advised > ceiling;
|
|
158
|
+
}
|
|
159
|
+
|
|
153
160
|
// Decides whether `caught` may be re-issued: returns the error to throw (the
|
|
154
161
|
// caught one, the exhausted-marked one on the last attempt, or the model
|
|
155
162
|
// fallback trigger) or null when the next attempt may run — and only then
|
|
@@ -206,6 +213,16 @@ function retryVerdict(caught, { opts, attempt, maxAttempts, state }) {
|
|
|
206
213
|
const typedQuota = QUOTA_KINDS.has(providerErrorKind(caught, { quotaFlags: false }).kind);
|
|
207
214
|
if (typedQuota || opts.retry429 === false || status !== 429) return caught;
|
|
208
215
|
}
|
|
216
|
+
// Opt-in `maxRetryAfterMs`: a server wait longer than the caller will sit
|
|
217
|
+
// through is a lock-out, not a retry. It surfaces now (with its advice for
|
|
218
|
+
// the notice and the account pool) instead of parking the send for it.
|
|
219
|
+
if (exceedsAdviceCeiling(caught, opts)) {
|
|
220
|
+
return markProviderRecoveryExhausted(caught, {
|
|
221
|
+
owner: opts.recoveryOwner || 'withRetry',
|
|
222
|
+
attempts: attempt + 1,
|
|
223
|
+
terminal: true,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
209
226
|
// Background calls (titles and the like) never retry an overload: during a
|
|
210
227
|
// capacity cascade every retry multiplies gateway load, and nobody is
|
|
211
228
|
// waiting on the result.
|
|
@@ -23,9 +23,11 @@ tool and brief each agent as the Lead rules describe.`;
|
|
|
23
23
|
export const AGENT_MODE_DELEGATION = `# Delegation
|
|
24
24
|
|
|
25
25
|
Handle conversation yourself. Delegate specialised work to sub-agents. Hand
|
|
26
|
-
substantial coding to a new
|
|
26
|
+
substantial coding to a new agent task session (\`session create\`) with
|
|
27
27
|
a self-contained brief: start it when already requested, otherwise suggest it. Then relay the
|
|
28
|
-
result in your own words
|
|
28
|
+
result in your own words. Work sent to an existing session (\`session send\`)
|
|
29
|
+
reports back only when its result says reportsBack: true; promise to report
|
|
30
|
+
only then.`;
|
|
29
31
|
|
|
30
32
|
/** Mixy only: it also routes requests to the user's contact agents. */
|
|
31
33
|
export const MIXY_CONTACTS_INTRO = `You also coordinate the user's contact agents: route a request to the
|
|
@@ -73,9 +73,9 @@ export const SESSION_TOOL = {
|
|
|
73
73
|
openWorldHint: false,
|
|
74
74
|
agentHidden: true,
|
|
75
75
|
},
|
|
76
|
-
trigger: 'Session lookup/control,
|
|
76
|
+
trigger: 'Session lookup/control, agent task sessions, title, compact, fork, Project and worktrees.',
|
|
77
77
|
description:
|
|
78
|
-
'Inspect sessions or control another by exact ID, never a guessed one. send acknowledges queue intake, not completion. Cancel/delete and worktree actions require the user’s request. Other actions affect only the current session.',
|
|
78
|
+
'Inspect sessions or control another by exact ID, never a guessed one. send acknowledges queue intake, not completion; promise a report of it only when its result has reportsBack: true. Cancel/delete and worktree actions require the user’s request. Other actions affect only the current session. Pass files on with attachments; a reported turn returns the files it produced as attachments.',
|
|
79
79
|
inputSchema: {
|
|
80
80
|
type: 'object',
|
|
81
81
|
properties: {
|
|
@@ -102,7 +102,7 @@ export const SESSION_TOOL = {
|
|
|
102
102
|
'prune-worktrees',
|
|
103
103
|
],
|
|
104
104
|
description:
|
|
105
|
-
'status defaults to current; list pages sessions; read returns latest items; send queues prompt; cancel stops the turn; delete removes the session. create (
|
|
105
|
+
'status defaults to current; list pages sessions; read returns latest items; send queues prompt (reportsBack: the turn result reports back here); cancel stops the turn; delete removes the session. create (bot conversations) starts a visible agent task session from brief and reports its turns back; unknown Project: list-projects first, still ambiguous: ask the user. compact runs after this turn; fork copies it without switching. merge-worktree merges into the Project’s checked-out branch; remove-worktree returns to the Project (repeat on WORKTREE_REMOVAL_INCOMPLETE); list-worktrees includes retaining sessions; prune-worktrees removes only clean, merged, unretained worktrees.',
|
|
106
106
|
},
|
|
107
107
|
title: { type: 'string', description: 'rename: new current-session title; create: task title.' },
|
|
108
108
|
session_id: {
|
|
@@ -147,8 +147,18 @@ export const SESSION_TOOL = {
|
|
|
147
147
|
value: { type: 'boolean', description: 'favorite/archive: on or off.' },
|
|
148
148
|
brief: { type: 'string', description: 'create: self-contained instruction, the first message.' },
|
|
149
149
|
project: { type: 'string', description: 'create: registered Project name or existing directory.' },
|
|
150
|
-
model: {
|
|
150
|
+
model: {
|
|
151
|
+
type: 'string',
|
|
152
|
+
description: 'create: provider/model or preset; default is the Lead model (else the last-used one).',
|
|
153
|
+
},
|
|
151
154
|
notify: { type: 'boolean', description: 'create: report results back, default true.' },
|
|
155
|
+
attachments: {
|
|
156
|
+
type: 'array',
|
|
157
|
+
items: { type: 'string' },
|
|
158
|
+
maxItems: 8,
|
|
159
|
+
description:
|
|
160
|
+
'send/create: files the target turn receives as real attachments (images, PDF/Office, text): an attachment of this conversation by its file name (a user upload, e.g. its [Image: source: name]) or id (a reported file’s [attachment:id]), or a file path inside this session’s folders. Secret files are refused.',
|
|
161
|
+
},
|
|
152
162
|
},
|
|
153
163
|
additionalProperties: false,
|
|
154
164
|
},
|
|
@@ -9,6 +9,15 @@ import { readJsonSafe, readTextSafe } from '../fs-utils.mjs';
|
|
|
9
9
|
import { SHARED_SCAN_REVALIDATE_MS, createSharedScanCache } from './shared-scan-cache.mjs';
|
|
10
10
|
import { isHiddenAgent } from '../../internal-agents.mjs';
|
|
11
11
|
import { isAgentDisabled } from '../../../../shared/agent-route-config.mjs';
|
|
12
|
+
import { agentDefinitionKind } from '../../../../shared/agent-kind.mjs';
|
|
13
|
+
import {
|
|
14
|
+
BOTS_DIR,
|
|
15
|
+
MIXY_BOT_ID,
|
|
16
|
+
botStoreActive,
|
|
17
|
+
isStoreBot,
|
|
18
|
+
migratedBotIds,
|
|
19
|
+
userDefinitionDir,
|
|
20
|
+
} from '../../../../shared/agent-store.mjs';
|
|
12
21
|
import {
|
|
13
22
|
AGENT_DELETED_MARKER,
|
|
14
23
|
AGENT_ROLE_IDS,
|
|
@@ -21,17 +30,22 @@ import {
|
|
|
21
30
|
setAgentDefinitionCache,
|
|
22
31
|
} from '../workflow-ids.mjs';
|
|
23
32
|
|
|
24
|
-
// Yields its fs requests; see shared-scan-cache.mjs.
|
|
25
|
-
|
|
33
|
+
// Yields its fs requests; see shared-scan-cache.mjs. With the bot store
|
|
34
|
+
// active, bots/ is scanned too and the agents/ originals of the ids the split
|
|
35
|
+
// copied are skipped (agent-store.mjs).
|
|
36
|
+
function* scanCustomAgentIds(userRoot, shippedRoot, botsRoot = null, migrated = new Set()) {
|
|
26
37
|
const ids = new Set();
|
|
27
|
-
for (const root of [userRoot, shippedRoot]) {
|
|
38
|
+
for (const root of [...(botsRoot ? [botsRoot] : []), userRoot, shippedRoot]) {
|
|
28
39
|
const entries = yield { op: 'readdir', path: root };
|
|
29
40
|
for (const entry of entries) {
|
|
30
41
|
if (!entry.isDirectory()) continue;
|
|
31
42
|
const id = normalizeWorkflowId(entry.name);
|
|
32
|
-
// `lead` is
|
|
43
|
+
// `lead` is the Lead's own folder, `mixy` Mixy's: neither is an agent.
|
|
33
44
|
if (!id || id === 'lead' || AGENT_ROLE_IDS.has(id) || isHiddenAgent(id)) continue;
|
|
34
|
-
if (root
|
|
45
|
+
if (root === botsRoot && id === MIXY_BOT_ID) continue;
|
|
46
|
+
if (root === userRoot && migrated.has(id)) continue;
|
|
47
|
+
if (root === botsRoot && entry.name !== id) continue;
|
|
48
|
+
if (root === shippedRoot && (yield { op: 'exists', path: join(userRoot, id, AGENT_DELETED_MARKER) })) continue;
|
|
35
49
|
if (!(yield { op: 'exists', path: join(root, entry.name, 'AGENT.md') })) continue;
|
|
36
50
|
ids.add(id);
|
|
37
51
|
}
|
|
@@ -57,15 +71,24 @@ const agentListCache = createSharedScanCache();
|
|
|
57
71
|
|
|
58
72
|
export function createWorkflowAgents({ rootDir, dataDir, readMarkdownDocument, normalizeAgentPermissionOrNone }) {
|
|
59
73
|
function agentSourceDirs(dir, id) {
|
|
74
|
+
// A store bot reads only its own folder (agent-store.mjs).
|
|
75
|
+
if (isStoreBot(dir || dataDir, id)) return [userDefinitionDir(dir || dataDir, id)];
|
|
60
76
|
const userDir = join(dir || dataDir, 'agents', id);
|
|
61
77
|
if (existsSync(join(userDir, AGENT_DELETED_MARKER))) return [userDir];
|
|
62
78
|
return [userDir, join(rootDir, 'agents', id)];
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
function agentScan(dir) {
|
|
66
|
-
const
|
|
82
|
+
const base = dir || dataDir;
|
|
83
|
+
const userRoot = join(base, 'agents');
|
|
67
84
|
const shippedRoot = join(rootDir, 'agents');
|
|
68
|
-
|
|
85
|
+
const store = botStoreActive(base);
|
|
86
|
+
const migrated = store ? migratedBotIds(base) : new Set();
|
|
87
|
+
const botsRoot = store ? join(base, BOTS_DIR) : null;
|
|
88
|
+
return {
|
|
89
|
+
key: `${userRoot}\n${shippedRoot}\n${botsRoot || ''}\n${[...migrated].join(',')}`,
|
|
90
|
+
makeScan: () => scanCustomAgentIds(userRoot, shippedRoot, botsRoot, migrated),
|
|
91
|
+
};
|
|
69
92
|
}
|
|
70
93
|
|
|
71
94
|
function listCustomAgentIds(dir) {
|
|
@@ -101,6 +124,13 @@ export function createWorkflowAgents({ rootDir, dataDir, readMarkdownDocument, n
|
|
|
101
124
|
description: clean(manifest.description) || slot?.description || '',
|
|
102
125
|
permission: normalizeAgentPermissionOrNone(doc.frontmatter.permission),
|
|
103
126
|
frontmatter: doc.frontmatter,
|
|
127
|
+
// bot | agent | null, decided read-only (shared/agent-kind.mjs).
|
|
128
|
+
kind: agentDefinitionKind({
|
|
129
|
+
frontmatterKind: doc.frontmatter.kind,
|
|
130
|
+
...(d !== join(rootDir, 'agents', agentId)
|
|
131
|
+
? { dataDir: dir || dataDir, shippedRoot: rootDir, id: agentId, manifest, body }
|
|
132
|
+
: {}),
|
|
133
|
+
}),
|
|
104
134
|
body,
|
|
105
135
|
// Optional PERSONA.md beside AGENT.md (same dir, same cache entry).
|
|
106
136
|
persona: (readTextSafe(join(d, 'PERSONA.md')) || '').trim(),
|
|
@@ -63,16 +63,18 @@ export function createWorkflowContext({ packs, agents }) {
|
|
|
63
63
|
function leadPromptParts(config, dir, { agentMode = false, mixy = false } = {}) {
|
|
64
64
|
const mode = configuredOrchestrationMode(config);
|
|
65
65
|
const ids = agents.delegatableAgentIds(config, dir);
|
|
66
|
+
const isContact = (id) => isContactAgentId(id, agents.loadAgentDefinition(dir, id)?.kind);
|
|
66
67
|
if (!agentMode) {
|
|
67
|
-
|
|
68
|
+
// Bots are never a coding Lead's agents: only Mixy hands them work.
|
|
69
|
+
const workflowIds = ids.filter((id) => !isContact(id));
|
|
70
|
+
const catalog = agentCatalog(dir, workflowIds);
|
|
68
71
|
return {
|
|
69
72
|
// Solo without agents has nothing to delegate to on request either.
|
|
70
|
-
orchestrationContext: mode === 'none' && !
|
|
73
|
+
orchestrationContext: mode === 'none' && !workflowIds.length ? '' : orchestrationInstructions(mode),
|
|
71
74
|
agentCatalogContext: catalog ? `# Available Agents\n\n${catalog}` : '',
|
|
72
75
|
contactCatalogContext: '',
|
|
73
76
|
};
|
|
74
77
|
}
|
|
75
|
-
const isContact = (id) => isContactAgentId(id, agents.loadAgentDefinition(dir, id)?.frontmatter?.kind);
|
|
76
78
|
const subAgents = agentCatalog(
|
|
77
79
|
dir,
|
|
78
80
|
ids.filter((id) => !isContact(id))
|
|
@@ -29,6 +29,10 @@ export interface RoomRow {
|
|
|
29
29
|
/** Host → log only: a user row's files as the composer sent them, which the
|
|
30
30
|
* log's intake stores (agent-room-delivery.mjs) and replaces with the above. */
|
|
31
31
|
upload?: { parts: unknown[]; pastedImages?: Record<string, unknown>; pastedTexts?: Record<string, unknown> };
|
|
32
|
+
/** Host → log only: a speech row's member session and the start of the
|
|
33
|
+
* turn that spoke; the log's intake stores the files that turn produced
|
|
34
|
+
* (agent-room-delivery.mjs) as the row's `parts` / `attachments`. */
|
|
35
|
+
artifactsFrom?: { sessionId: string; since: number };
|
|
32
36
|
text: string;
|
|
33
37
|
}
|
|
34
38
|
/** A stored file reference (attachments/store.mjs): an image, a file, or an
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
// (`sess_rmem_<room>_<agent>`) and is handed only the rows new since it last
|
|
7
7
|
// spoke. What another agent said reaches a member as DATA inside an explicit
|
|
8
8
|
// boundary, with every harness tag escaped and every line relabelled, so a peer
|
|
9
|
-
// can never forge a control frame or pose as the user. Files come
|
|
10
|
-
// user's own rows
|
|
11
|
-
//
|
|
9
|
+
// can never forge a control frame or pose as the user. Files come with the
|
|
10
|
+
// user's own rows and with a member's speech (the files its turn produced,
|
|
11
|
+
// session-runtime/handoff-attachments.mjs): stored references, handed as
|
|
12
|
+
// content to the members the row reaches, once each.
|
|
12
13
|
import { escapeHarnessTags } from '../../../shared/shell-output-summary.mjs';
|
|
13
14
|
import { normalizeMessageAttachments, transcriptAttachmentFields } from '../../../shared/transcript-metadata.mjs';
|
|
14
15
|
import { agentHomeSessionId } from './agent-home.mjs';
|
|
@@ -465,6 +466,12 @@ export function roomTurnPrompt({
|
|
|
465
466
|
if (shareTools) {
|
|
466
467
|
for (const tool of row.tools || []) lines.push(...peerLines(`(tool ${label(tool.name)}) ${tool.summary}`));
|
|
467
468
|
}
|
|
469
|
+
// The files this member's turn produced follow its words, still data.
|
|
470
|
+
const files = storedParts(row.parts);
|
|
471
|
+
if (files.length) {
|
|
472
|
+
lines.push(...peerLines(`(files from ${label(row.name || row.speaker)} follow)`));
|
|
473
|
+
content.push({ type: 'text', text: lines.splice(0).join('\n') }, ...files);
|
|
474
|
+
}
|
|
468
475
|
continue;
|
|
469
476
|
}
|
|
470
477
|
if (open) lines.push('</room-transcript>');
|
|
@@ -547,9 +554,11 @@ export function normalizeRoomRow(row = {}) {
|
|
|
547
554
|
.map((target) => ({ id: cleanName(target.id), name: cleanName(target.name) }))
|
|
548
555
|
.filter((target, index, list) => target.id && list.findIndex((other) => other.id === target.id) === index)
|
|
549
556
|
.slice(0, ROOM_MAX_MEMBERS);
|
|
550
|
-
// Files are the user's
|
|
551
|
-
|
|
552
|
-
const
|
|
557
|
+
// Files are the user's uploads or the files a member's turn produced: what
|
|
558
|
+
// the log's intake stored for the row.
|
|
559
|
+
const carriesFiles = row.kind === 'user' || row.kind === 'speech';
|
|
560
|
+
const parts = carriesFiles ? storedParts(row.parts) : [];
|
|
561
|
+
const attachments = carriesFiles ? normalizeMessageAttachments(row.attachments) : [];
|
|
553
562
|
const images = row.kind === 'user' ? storedImages(row.images) : [];
|
|
554
563
|
return {
|
|
555
564
|
id,
|
|
@@ -623,8 +632,8 @@ export function roomRowItem(message, id) {
|
|
|
623
632
|
room: { ...room, id: messageId },
|
|
624
633
|
...(messageId ? { messageId } : {}),
|
|
625
634
|
...(Number.isFinite(at) ? { at } : {}),
|
|
626
|
-
// A user row's files, in the fields a chat prompt's row draws them from.
|
|
627
|
-
...(room.kind === 'user' ? transcriptAttachmentFields(message.meta.transcript) : {}),
|
|
635
|
+
// A user row's (or member speech's) files, in the fields a chat prompt's row draws them from.
|
|
636
|
+
...(room.kind === 'user' || room.kind === 'speech' ? transcriptAttachmentFields(message.meta.transcript) : {}),
|
|
628
637
|
...(room.kind === 'user' && Array.isArray(images) && images.length ? { images } : {}),
|
|
629
638
|
};
|
|
630
639
|
}
|
|
@@ -5,13 +5,18 @@
|
|
|
5
5
|
// turns an outbound call (outbound-policy.mjs) into an `ask` whatever the
|
|
6
6
|
// hooks or the agent's permission said short of a deny, unless the agent's
|
|
7
7
|
// permission for that action kind (store.mjs) is `allow`; `block` denies it
|
|
8
|
-
// without a card.
|
|
8
|
+
// without a card. The outbound rule applies ONLY while a bot acts (see
|
|
9
|
+
// actingBotId); an ordinary coding session's outbound call passes with the
|
|
10
|
+
// hook decision unchanged. Other surfaces (group rooms) call it with their own
|
|
9
11
|
// decision to get the same outbound rule. After the user answered,
|
|
10
12
|
// recordHumanDecision stores a "don't ask again" grant and the audit line. Unattended runs (schedule, webhook, room member) register a
|
|
11
13
|
// port here (session-runtime/held-requests.mjs) that turns their asks into
|
|
12
14
|
// held requests instead of an approval card.
|
|
13
15
|
import { MIXY_AGENT_ID, agentIdOfHomeSession } from '../agent-home.mjs';
|
|
14
16
|
import { serverConnector } from '../../mcp/server-connectors.mjs';
|
|
17
|
+
import { readAgentDefinitionKind } from '../../../../shared/agent-kind.mjs';
|
|
18
|
+
import { isContactAgentId } from '../../../../shared/agent-route-config.mjs';
|
|
19
|
+
import { mixdogRoot, resolvePluginData } from '../../../../shared/plugin-paths.mjs';
|
|
15
20
|
import { roomMemberOfSession } from '../agent-room.mjs';
|
|
16
21
|
import { classifyOutboundCall, isRememberableAction } from './outbound-policy.mjs';
|
|
17
22
|
import { humanGateStore } from './store.mjs';
|
|
@@ -33,12 +38,33 @@ export function unattendedApprovalPort() {
|
|
|
33
38
|
return unattendedPort;
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
/** The agent a session is bound to: its unattended run's owner, a room
|
|
42
|
+
* member, a home thread's agent; '' for any other (coding) session. */
|
|
43
|
+
function boundAgentId(sessionId) {
|
|
44
|
+
const run = unattendedPort?.runOf?.(sessionId);
|
|
45
|
+
if (run?.agentId) return run.agentId;
|
|
46
|
+
return roomMemberOfSession(sessionId)?.agentId || agentIdOfHomeSession(sessionId) || '';
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
/** The agent a session acts for: its unattended run's owner, a room member,
|
|
37
50
|
* a home thread's agent, else Mixy. */
|
|
38
51
|
export function agentIdForSession(sessionId) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
return boundAgentId(sessionId) || MIXY_AGENT_ID;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The bot a session acts for, or '' when none: Mixy in her own home thread
|
|
56
|
+
* (or her unattended run / room member session), or a contact agent (`kind:
|
|
57
|
+
* bot`, isContactAgentId) in its home thread, room member session or
|
|
58
|
+
* unattended run. A coding Lead (also id 'lead'), its workflow agents and
|
|
59
|
+
* their sub-agent sessions are bound to nothing here, so they are no bot.
|
|
60
|
+
* A sub-agent of a bot has its own session id and no link back to the
|
|
61
|
+
* caller in this layer, so it is not gated either. */
|
|
62
|
+
function actingBotId(sessionId) {
|
|
63
|
+
const id = boundAgentId(sessionId);
|
|
64
|
+
if (!id) return '';
|
|
65
|
+
if (id === MIXY_AGENT_ID) return id;
|
|
66
|
+
const kind = readAgentDefinitionKind({ dataDir: resolvePluginData(), shippedRoot: mixdogRoot(), id });
|
|
67
|
+
return isContactAgentId(id, kind) ? id : '';
|
|
42
68
|
}
|
|
43
69
|
|
|
44
70
|
const actionOf = (decision) => String(decision?.action || decision?.decision || '').toLowerCase();
|
|
@@ -97,7 +123,8 @@ export async function humanGateDecision(call, decision, ctx = {}) {
|
|
|
97
123
|
if (action === 'deny' || action === 'block') return { decision, request: null, gate: null };
|
|
98
124
|
const agentId = agentIdForSession(ctx.sessionId);
|
|
99
125
|
const annotations = ctx.annotations ?? (await mcpAnnotations(call?.name, ctx.mcpScopeId));
|
|
100
|
-
const
|
|
126
|
+
const classified = classifyOutboundCall({ name: call?.name, args: call?.args, annotations });
|
|
127
|
+
const outbound = classified && actingBotId(ctx.sessionId) ? classified : null;
|
|
101
128
|
if (!outbound) {
|
|
102
129
|
if (action !== 'ask') return { decision, request: null, gate: null };
|
|
103
130
|
return {
|
|
@@ -8,6 +8,7 @@ import { resolveLiveToolCwd } from './tool-exec.mjs';
|
|
|
8
8
|
import { createSteeringDrain } from './steering.mjs';
|
|
9
9
|
import { createNoToolTurnResolver } from './no-tool-turn.mjs';
|
|
10
10
|
import { classifyTerminationReason } from './termination.mjs';
|
|
11
|
+
import { routeUsageSegments } from './usage.mjs';
|
|
11
12
|
|
|
12
13
|
// Consecutive identical-AND-failing tool calls (same name+args, error result)
|
|
13
14
|
// tolerated across iterations before the loop refuses to re-execute and steers
|
|
@@ -153,5 +154,13 @@ export function finishLoop(state) {
|
|
|
153
154
|
providerStateUpdated: state.providerStateUpdated,
|
|
154
155
|
terminationReason: classifyTerminationReason(response, { sessionAgent: state.sessionAgent }),
|
|
155
156
|
providerContinuations: state.noToolTurn.providerContinuations,
|
|
157
|
+
...(state.routeUsageHops?.length
|
|
158
|
+
? {
|
|
159
|
+
usageSegments: routeUsageSegments(state.routeUsageHops, state.lastUsage, {
|
|
160
|
+
provider: state.sessionRef?.provider,
|
|
161
|
+
model: state.sessionRef?.model,
|
|
162
|
+
}),
|
|
163
|
+
}
|
|
164
|
+
: {}),
|
|
156
165
|
};
|
|
157
166
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { routeFallbackVerdict } from '../../../../shared/model-route-fallback.mjs';
|
|
2
|
+
import { providerErrorKind } from '../../providers/provider-error-kind.mjs';
|
|
3
|
+
import { displayModelName } from '../../../../shared/model-display.mjs';
|
|
4
|
+
import { presentErrorText } from '../../../../shared/err-text.mjs';
|
|
2
5
|
import { getProvider, initProviders } from '../../providers/registry.mjs';
|
|
3
6
|
import { loadConfig } from '../../config.mjs';
|
|
4
7
|
import { ensureProviderEnabled } from '../../runtime-core/config-helpers.mjs';
|
|
@@ -7,19 +10,107 @@ import { pinSessionWireRoute } from '../../providers/wire-route.mjs';
|
|
|
7
10
|
import { buildRequestSendOptions } from '../manager/ask-turn-loop-options.mjs';
|
|
8
11
|
import { prepareTurnEffortConfiguration } from '../../providers/effort-configuration.mjs';
|
|
9
12
|
import { resolveSessionContextMeta } from '../manager/context-meta.mjs';
|
|
13
|
+
import { stripProviderReplayMessages } from '../manager/provider-replay.mjs';
|
|
14
|
+
|
|
15
|
+
// The short reason a notice gives for leaving a model, by provider kind.
|
|
16
|
+
const HOP_REASONS = {
|
|
17
|
+
quota_exhausted: 'usage limit reached',
|
|
18
|
+
entitlement: 'not included in the plan',
|
|
19
|
+
billing: 'out of credits',
|
|
20
|
+
rate_limited: 'rate-limited',
|
|
21
|
+
overloaded: 'unavailable',
|
|
22
|
+
transient: 'unavailable',
|
|
23
|
+
service_tier_unavailable: 'unavailable',
|
|
24
|
+
auth: 'sign-in failed',
|
|
25
|
+
permission: 'access denied',
|
|
26
|
+
invalid_request: 'model not available',
|
|
27
|
+
context_overflow: 'context too long',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const routeLabel = (route) => displayModelName(route.model, route.provider) || `${route.provider}/${route.model}`;
|
|
31
|
+
|
|
32
|
+
/** The muted transcript line for a hop: `<from> <reason> — switched to <to>.` */
|
|
33
|
+
export function routeFallbackNoticeText(from, to, error) {
|
|
34
|
+
const reason = HOP_REASONS[providerErrorKind(error).kind] || 'unavailable';
|
|
35
|
+
return `${routeLabel(from)} ${reason} — switched to ${routeLabel(to)}.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function routeFailure(route, error) {
|
|
39
|
+
return {
|
|
40
|
+
provider: route.provider,
|
|
41
|
+
model: route.model,
|
|
42
|
+
kind: providerErrorKind(error).kind,
|
|
43
|
+
error: presentErrorText(error),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The chain ended after at least one hop: the final error keeps its own kind
|
|
48
|
+
// and names every model that was tried and why each failed. `skipped` is the
|
|
49
|
+
// next route when the chain stopped before it (a context overflow whose next
|
|
50
|
+
// window is no larger), so the text says why instead of "every model failed".
|
|
51
|
+
function annotateEndedChain(session, error, skipped = null) {
|
|
52
|
+
const tried = session?.routeFallbackFailures;
|
|
53
|
+
if (!tried?.length || !error || typeof error !== 'object' || error.routeFallbackAttempts) return;
|
|
54
|
+
const attempts = [...tried, routeFailure(session, error)];
|
|
55
|
+
try {
|
|
56
|
+
error.routeFallbackAttempts = attempts;
|
|
57
|
+
const lines = attempts.map((attempt) => `${routeLabel(attempt)}: ${attempt.error}`).join('; ');
|
|
58
|
+
const head = skipped
|
|
59
|
+
? `Fallback chain stopped before ${routeLabel(skipped)} (its context window is not larger) — tried`
|
|
60
|
+
: 'Every model in the fallback chain failed —';
|
|
61
|
+
error.message = `${error.message}\n${head} ${lines}`;
|
|
62
|
+
if (skipped) error.routeFallbackStoppedBefore = { provider: skipped.provider, model: skipped.model };
|
|
63
|
+
} catch {
|
|
64
|
+
/* frozen errors keep their own message */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A context overflow moves only to a model whose window is larger.
|
|
69
|
+
async function nextContextGrows(state, next) {
|
|
70
|
+
const config = ensureProviderEnabled(loadConfig(), next.provider);
|
|
71
|
+
await initProviders(config.providers, { signal: state.signal });
|
|
72
|
+
state.throwIfAborted();
|
|
73
|
+
const provider = getProvider(next.provider);
|
|
74
|
+
if (!provider) return false;
|
|
75
|
+
const current = resolveSessionContextMeta(state.provider, state.sessionRef.model, state.sessionRef).rawContextWindow;
|
|
76
|
+
const window = resolveSessionContextMeta(provider, next.model, next).rawContextWindow;
|
|
77
|
+
return Number(window) > Number(current || state.sessionRef.selectedContextWindow || 0);
|
|
78
|
+
}
|
|
10
79
|
|
|
11
80
|
export async function advanceAgentRoute(state, error) {
|
|
12
81
|
const session = state.sessionRef;
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
82
|
+
if (!session?.agent || !session.routeFallbacks?.length) return false;
|
|
83
|
+
const index = session.routeFallbackIndex || 0;
|
|
84
|
+
const next = session.routeFallbacks[index];
|
|
85
|
+
// Only THIS request must have produced nothing (its stream outcome says so):
|
|
86
|
+
// earlier requests of the turn may have run tools. Their calls and results
|
|
87
|
+
// are already in the live turn, which the next model continues from, so no
|
|
88
|
+
// completed tool runs again.
|
|
89
|
+
const verdict = routeFallbackVerdict(error, { signal: state.signal });
|
|
90
|
+
const hops = verdict === 'hop' || (verdict === 'context' && next && (await nextContextGrows(state, next)));
|
|
91
|
+
if (!next || !hops) {
|
|
92
|
+
if (verdict !== 'stay') annotateEndedChain(session, error, next || null);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
17
95
|
state.throwIfAborted();
|
|
18
96
|
const config = ensureProviderEnabled(loadConfig(), next.provider);
|
|
19
97
|
await initProviders(config.providers, { signal: state.signal });
|
|
20
98
|
state.throwIfAborted();
|
|
21
99
|
const provider = getProvider(next.provider);
|
|
22
100
|
if (!provider) throw new Error(`Provider "${next.provider}" not available`);
|
|
101
|
+
const failed = routeFailure(session, error);
|
|
102
|
+
// The left model's share of the turn ends here: the running total at the
|
|
103
|
+
// switch bounds it, so each model's tokens are booked at its own rates.
|
|
104
|
+
state.routeUsageHops = [
|
|
105
|
+
...(state.routeUsageHops || []),
|
|
106
|
+
{ provider: session.provider, model: session.model, usage: state.lastUsage ? { ...state.lastUsage } : null },
|
|
107
|
+
];
|
|
108
|
+
const notice = routeFallbackNoticeText(session, next, error);
|
|
109
|
+
// Saved with the new route: a crash before the live turn is checkpointed
|
|
110
|
+
// again recovers that turn without the previous provider's native state.
|
|
111
|
+
if (session.activeTurnCheckpoint && typeof session.activeTurnCheckpoint === 'object') {
|
|
112
|
+
session.activeTurnCheckpoint = { ...session.activeTurnCheckpoint, routeChanged: true };
|
|
113
|
+
}
|
|
23
114
|
const oldOptions = buildRequestSendOptions({
|
|
24
115
|
sessionId: state.sessionId,
|
|
25
116
|
session,
|
|
@@ -41,9 +132,12 @@ export async function advanceAgentRoute(state, error) {
|
|
|
41
132
|
);
|
|
42
133
|
if (!updated) throw new Error('agent session closed during model fallback');
|
|
43
134
|
updated.routeFallbackIndex = index + 1;
|
|
135
|
+
updated.routeFallbackFailures = [...(session.routeFallbackFailures || []), failed];
|
|
44
136
|
await pinSessionWireRoute(updated, provider);
|
|
45
137
|
state.throwIfAborted();
|
|
46
|
-
|
|
138
|
+
// The live turn (prompt, assistant tool calls, tool results) carries on;
|
|
139
|
+
// only the previous provider's native replay state is dropped from it.
|
|
140
|
+
state.messages.splice(0, state.messages.length, ...stripProviderReplayMessages(state.messages));
|
|
47
141
|
updated.liveTurnMessages = state.messages;
|
|
48
142
|
state.sessionRef = updated;
|
|
49
143
|
state.provider = provider;
|
|
@@ -67,6 +161,14 @@ export async function advanceAgentRoute(state, error) {
|
|
|
67
161
|
signal: state.signal,
|
|
68
162
|
})
|
|
69
163
|
);
|
|
164
|
+
try {
|
|
165
|
+
process.stderr.write(
|
|
166
|
+
`[route-fallback] ${failed.provider}/${failed.model} (${failed.kind}) -> ${updated.provider}/${updated.model}\n`
|
|
167
|
+
);
|
|
168
|
+
state.opts.onStageChange?.('notice', { tone: 'info', message: notice });
|
|
169
|
+
} catch {
|
|
170
|
+
/* display-only */
|
|
171
|
+
}
|
|
70
172
|
state.opts.onStageChange?.('connecting', {
|
|
71
173
|
provider: updated.provider,
|
|
72
174
|
model: updated.model,
|
|
@@ -7,7 +7,6 @@ import { traceCacheBreak } from '../../cache-break-trace.mjs';
|
|
|
7
7
|
import { resolveLiveToolCwd } from './tool-exec.mjs';
|
|
8
8
|
import { notifySkillsApplied, prepareExplicitSkills, recordAppliedSkills } from '../explicit-skills.mjs';
|
|
9
9
|
import { addUsage, usageDeltaEvent } from './usage.mjs';
|
|
10
|
-
import { isVisibleStreamProgress } from '../../../../shared/stream-progress.mjs';
|
|
11
10
|
import { prepareProviderRequest } from './request-boundary.mjs';
|
|
12
11
|
import { applyStepEffort } from './step-auto-effort.mjs';
|
|
13
12
|
import { projectProviderRequest } from './request-projection.mjs';
|
|
@@ -182,15 +181,10 @@ export async function sendProviderRequest(state, round) {
|
|
|
182
181
|
tools: round.sendTools,
|
|
183
182
|
opts,
|
|
184
183
|
});
|
|
185
|
-
// The
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
const observeStreamDelta = (kind, ...rest) => {
|
|
190
|
-
if (!firstVisibleAt && isVisibleStreamProgress(kind)) firstVisibleAt = Date.now();
|
|
191
|
-
return callerOnStreamDelta?.(kind, ...rest);
|
|
192
|
-
};
|
|
193
|
-
opts.onStreamDelta = observeStreamDelta;
|
|
184
|
+
// The request's time for the speed figures, the wait for the first token
|
|
185
|
+
// included: the span the usage ledger recorded as `duration_ms` (stamped on
|
|
186
|
+
// the response); wall time only for a response no ledger row timed.
|
|
187
|
+
const requestStartedAt = Date.now();
|
|
194
188
|
let result;
|
|
195
189
|
try {
|
|
196
190
|
result = await runWithProviderRequestToolsScope(round.requestToolScope, () =>
|
|
@@ -218,8 +212,6 @@ export async function sendProviderRequest(state, round) {
|
|
|
218
212
|
sideQuestion?.discard();
|
|
219
213
|
if (!(await advanceAgentRoute(state, error))) throw error;
|
|
220
214
|
result = { action: 'retry_route' };
|
|
221
|
-
} finally {
|
|
222
|
-
if (opts.onStreamDelta === observeStreamDelta) opts.onStreamDelta = callerOnStreamDelta;
|
|
223
215
|
}
|
|
224
216
|
if (result.action === 'proceed') sideQuestion?.settle(result.response);
|
|
225
217
|
else sideQuestion?.discard();
|
|
@@ -230,7 +222,9 @@ export async function sendProviderRequest(state, round) {
|
|
|
230
222
|
prefixGuardCandidate,
|
|
231
223
|
sendStartedAt,
|
|
232
224
|
sendEndedAt,
|
|
233
|
-
|
|
225
|
+
requestMs: Number.isFinite(result.response?.usageDurationMs)
|
|
226
|
+
? Math.max(0, result.response.usageDurationMs)
|
|
227
|
+
: Math.max(0, sendEndedAt - requestStartedAt),
|
|
234
228
|
preSendMs,
|
|
235
229
|
toolResumeMs,
|
|
236
230
|
};
|
|
@@ -443,7 +437,7 @@ export function settleSendResult(state, round, sent) {
|
|
|
443
437
|
usage: reportedUsage,
|
|
444
438
|
sendTools: round.sendTools,
|
|
445
439
|
toolCalls: response.toolCalls?.length || 0,
|
|
446
|
-
|
|
440
|
+
requestMs: sent.requestMs,
|
|
447
441
|
})
|
|
448
442
|
)
|
|
449
443
|
);
|