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
|
@@ -41,7 +41,7 @@ export function usageDeltaEvent({
|
|
|
41
41
|
usage,
|
|
42
42
|
sendTools,
|
|
43
43
|
toolCalls = 0,
|
|
44
|
-
|
|
44
|
+
requestMs = 0,
|
|
45
45
|
}) {
|
|
46
46
|
return {
|
|
47
47
|
sessionId,
|
|
@@ -57,9 +57,10 @@ export function usageDeltaEvent({
|
|
|
57
57
|
deltaToolCalls: toolCalls,
|
|
58
58
|
...reasoningUsage(usage),
|
|
59
59
|
deltaPrompt: usage.promptTokens || 0,
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
60
|
+
// Request start → end of this response (first-token wait included, as the
|
|
61
|
+
// ledger's duration_ms); 0 when the call was not timed. Only timed output
|
|
62
|
+
// enters speed figures.
|
|
63
|
+
requestMs,
|
|
63
64
|
// A provider that cannot measure the prompt or its cache split says so;
|
|
64
65
|
// its zeros are unknowns, not readings (no cache hit can be derived).
|
|
65
66
|
promptKnown: usage.inputTokensKnown !== false && usage.cacheTokensKnown !== false,
|
|
@@ -79,6 +80,47 @@ export function usageDeltaEvent({
|
|
|
79
80
|
};
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
const SUMMED_USAGE_FIELDS = [
|
|
84
|
+
'inputTokens',
|
|
85
|
+
'outputTokens',
|
|
86
|
+
'cachedTokens',
|
|
87
|
+
'cacheWriteTokens',
|
|
88
|
+
'cacheWrite1hTokens',
|
|
89
|
+
'promptTokens',
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
/** What `total` added after the running total `earlier` (both from addUsage); null when nothing. */
|
|
93
|
+
export function usageSince(total, earlier) {
|
|
94
|
+
if (!total) return null;
|
|
95
|
+
if (!earlier) return { ...total };
|
|
96
|
+
const delta = {};
|
|
97
|
+
for (const field of SUMMED_USAGE_FIELDS) delta[field] = Math.max(0, (total[field] || 0) - (earlier[field] || 0));
|
|
98
|
+
if (total.reasoningTokens != null) {
|
|
99
|
+
delta.reasoningTokens = Math.max(0, total.reasoningTokens - (earlier.reasoningTokens ?? 0));
|
|
100
|
+
delta.reasoningTokensComplete = total.reasoningTokensComplete === true;
|
|
101
|
+
}
|
|
102
|
+
if (total.costUsd != null) delta.costUsd = Math.max(0, total.costUsd - (earlier.costUsd || 0));
|
|
103
|
+
if (total.contextTokens) delta.contextTokens = total.contextTokens;
|
|
104
|
+
return Object.values(delta).some((value) => typeof value === 'number' && value > 0) ? delta : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The turn's usage split per model when the route switched mid-turn:
|
|
109
|
+
* `hops` holds, per left model, the running total when it was left. Each
|
|
110
|
+
* segment carries only its own tokens so it is priced at its own rates.
|
|
111
|
+
*/
|
|
112
|
+
export function routeUsageSegments(hops, total, current) {
|
|
113
|
+
if (!Array.isArray(hops) || !hops.length) return null;
|
|
114
|
+
const boundaries = [...hops, { ...current, usage: total }];
|
|
115
|
+
return boundaries
|
|
116
|
+
.map((segment, index) => ({
|
|
117
|
+
provider: segment.provider,
|
|
118
|
+
model: segment.model,
|
|
119
|
+
usage: usageSince(segment.usage, index ? boundaries[index - 1].usage : null),
|
|
120
|
+
}))
|
|
121
|
+
.filter((segment) => segment.usage);
|
|
122
|
+
}
|
|
123
|
+
|
|
82
124
|
export function addUsage(total, usage) {
|
|
83
125
|
const delta = normalizeUsage(usage);
|
|
84
126
|
if (!delta) return total;
|
|
@@ -120,14 +120,24 @@ function recordAgentRuntimeCall(session, result) {
|
|
|
120
120
|
export async function recordAskUsage({ session, result, askStartedAt, turnSignal }) {
|
|
121
121
|
const prefixHashForLog = recordAgentRuntimeCall(session, result);
|
|
122
122
|
if (!result.usage) return;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
// A turn that switched models mid-way books one row per model, each priced
|
|
124
|
+
// at that model's rates; otherwise one row for the whole turn.
|
|
125
|
+
const segments = result.usageSegments?.length
|
|
126
|
+
? result.usageSegments
|
|
127
|
+
: [{ provider: session.provider, model: session.model, usage: result.usage }];
|
|
128
|
+
for (const segment of segments) {
|
|
129
|
+
await logSessionLlmUsage({
|
|
130
|
+
session:
|
|
131
|
+
segment.provider === session.provider && segment.model === session.model
|
|
132
|
+
? session
|
|
133
|
+
: { ...session, provider: segment.provider, model: segment.model },
|
|
134
|
+
usage: segment.usage,
|
|
135
|
+
startedAt: askStartedAt,
|
|
136
|
+
signal: turnSignal,
|
|
137
|
+
prefixHash: prefixHashForLog,
|
|
138
|
+
transportTiming: result.transportTiming || null,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
131
141
|
recordStandaloneStatusTelemetry(session, result, Date.now() - askStartedAt);
|
|
132
142
|
}
|
|
133
143
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Provider-native replay state on transcript messages: reasoning items,
|
|
2
|
+
// thinking blocks with their signatures, ordered native blocks, Gemini thought
|
|
3
|
+
// signatures on tool calls. It belongs to the provider (and the account) that
|
|
4
|
+
// produced it — another provider rejects it or, worse, a gateway of the same
|
|
5
|
+
// family validates it against its own key. A route change drops it; what the
|
|
6
|
+
// next provider needs (text, tool calls and their results, in order) stays,
|
|
7
|
+
// and each adapter rebuilds its own wire shape from that.
|
|
8
|
+
|
|
9
|
+
// Every message field a provider adapter replays or re-reads as native state.
|
|
10
|
+
const PROVIDER_REPLAY_FIELDS = [
|
|
11
|
+
'providerReplay',
|
|
12
|
+
'reasoning',
|
|
13
|
+
'thinkingBlocks',
|
|
14
|
+
'assistantBlocks',
|
|
15
|
+
'reasoningItems',
|
|
16
|
+
'reasoningContent',
|
|
17
|
+
'providerMetadata',
|
|
18
|
+
];
|
|
19
|
+
// Opaque provider tokens a tool call may carry (Gemini thought signatures).
|
|
20
|
+
const TOOL_CALL_SIGNATURE_FIELDS = ['thoughtSignature', 'thought_signature'];
|
|
21
|
+
|
|
22
|
+
const hasAny = (value, fields) => fields.some((field) => field in value);
|
|
23
|
+
|
|
24
|
+
function plainToolCalls(toolCalls) {
|
|
25
|
+
if (!Array.isArray(toolCalls) || !toolCalls.some((call) => call && hasAny(call, TOOL_CALL_SIGNATURE_FIELDS))) {
|
|
26
|
+
return toolCalls;
|
|
27
|
+
}
|
|
28
|
+
return toolCalls.map((call) => {
|
|
29
|
+
if (!call || !hasAny(call, TOOL_CALL_SIGNATURE_FIELDS)) return call;
|
|
30
|
+
const plain = { ...call };
|
|
31
|
+
for (const field of TOOL_CALL_SIGNATURE_FIELDS) delete plain[field];
|
|
32
|
+
return plain;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `message` without provider-native replay state; the same object when it carries none. */
|
|
37
|
+
export function stripProviderReplayMessage(message) {
|
|
38
|
+
if (!message || typeof message !== 'object') return message;
|
|
39
|
+
const toolCalls = plainToolCalls(message.toolCalls);
|
|
40
|
+
if (!hasAny(message, PROVIDER_REPLAY_FIELDS) && toolCalls === message.toolCalls) return message;
|
|
41
|
+
const plain = { ...message };
|
|
42
|
+
for (const field of PROVIDER_REPLAY_FIELDS) delete plain[field];
|
|
43
|
+
if (toolCalls !== message.toolCalls) plain.toolCalls = toolCalls;
|
|
44
|
+
return plain;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Replace, never mutate: delta saves see rewritten messages as changed. */
|
|
48
|
+
export function stripProviderReplayMessages(messages) {
|
|
49
|
+
return Array.isArray(messages) ? messages.map(stripProviderReplayMessage) : messages;
|
|
50
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from '../../agent-runtime/cache-strategy.mjs';
|
|
20
20
|
import { normalizeAutoClearConfig, resolveAutoClearIdleMs } from '../../runtime-core/config-helpers.mjs';
|
|
21
21
|
import { _buildBaseRules } from './rules-cache.mjs';
|
|
22
|
+
import { stripProviderReplayMessages } from './provider-replay.mjs';
|
|
22
23
|
import { composeSessionSystem, seedSessionMessages } from './session-prompt-composition.mjs';
|
|
23
24
|
import { agentProfileRevision, leadRosterRevision } from '../../context/role-instructions.mjs';
|
|
24
25
|
import {
|
|
@@ -241,7 +242,9 @@ export function createSession(createOpts) {
|
|
|
241
242
|
const surface = resolveSessionToolSurface(opts, { profile, toolPreset, modelName });
|
|
242
243
|
// The agent profile the prompt is composed from, read first: an edit landing
|
|
243
244
|
// during composition leaves the stamp behind, never ahead (refreshAgentProfile).
|
|
244
|
-
const agentProfile = surface.ownerIsAgent
|
|
245
|
+
const agentProfile = surface.ownerIsAgent
|
|
246
|
+
? ''
|
|
247
|
+
: agentProfileRevision(surface.resolvedAgent, { mixy: sessionMode === AGENT_MODE });
|
|
245
248
|
const prompt = composeSessionSystem(opts, { profile, providerName, modelName, surface, sessionMode });
|
|
246
249
|
const messages = seedSessionMessages(prompt, opts.files);
|
|
247
250
|
logSessionSurface(surface);
|
|
@@ -421,27 +424,11 @@ function resetSessionForRouteChange(id, session, previousModel, previousProvider
|
|
|
421
424
|
_forgetPreparedResume(id);
|
|
422
425
|
}
|
|
423
426
|
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
'providerReplay',
|
|
427
|
-
'reasoning',
|
|
428
|
-
'thinkingBlocks',
|
|
429
|
-
'assistantBlocks',
|
|
430
|
-
'reasoningItems',
|
|
431
|
-
'reasoningContent',
|
|
432
|
-
'providerMetadata',
|
|
433
|
-
];
|
|
434
|
-
|
|
435
|
-
// Provider-native replay (reasoning items, thinking blocks) belongs to the
|
|
436
|
-
// provider that produced it. Replace, never mutate, so delta saves rewrite.
|
|
427
|
+
// Provider-native replay (reasoning items, thinking blocks, signatures)
|
|
428
|
+
// belongs to the provider that produced it (see provider-replay.mjs).
|
|
437
429
|
function stripProviderReplay(session) {
|
|
438
430
|
if (!Array.isArray(session.messages)) return;
|
|
439
|
-
session.messages = session.messages
|
|
440
|
-
if (!message || !PROVIDER_REPLAY_FIELDS.some((field) => field in message)) return message;
|
|
441
|
-
const plain = { ...message };
|
|
442
|
-
for (const field of PROVIDER_REPLAY_FIELDS) delete plain[field];
|
|
443
|
-
return plain;
|
|
444
|
-
});
|
|
431
|
+
session.messages = stripProviderReplayMessages(session.messages);
|
|
445
432
|
}
|
|
446
433
|
|
|
447
434
|
// `options.crossProvider` is the explicit in-place continuation of an
|
|
@@ -139,6 +139,9 @@ export function sessionRolePromptFields(
|
|
|
139
139
|
// session running as that agent) carries its own AGENT.md role, and Mixy
|
|
140
140
|
// in agent mode its optional agents/lead/AGENT.md.
|
|
141
141
|
skipRoleCatalog: !ownerIsAgent && (!identity.agent || identity.agent === 'lead') && !(agentMode && mixy),
|
|
142
|
+
// Mixy's persona (bots/mixy/PERSONA.md) is Mixy's alone: keyed on Mixy's
|
|
143
|
+
// own agent-mode thread, never on the `lead` role a coding Lead shares.
|
|
144
|
+
skipPersona: !ownerIsAgent && (!identity.agent || identity.agent === 'lead') && !(agentMode && mixy),
|
|
142
145
|
agent: identity.agent,
|
|
143
146
|
profile: { skip: identity.profileSkip },
|
|
144
147
|
};
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
writeCheckpointHeader,
|
|
19
19
|
} from './turn-checkpoint-journal.mjs';
|
|
20
20
|
import { captureTurnCheckpointContextState, restoreTurnCheckpointContextState } from './turn-checkpoint-context.mjs';
|
|
21
|
+
import { stripProviderReplayMessages } from './provider-replay.mjs';
|
|
21
22
|
|
|
22
23
|
export { turnCheckpointPath, turnMessagesForCheckpoint };
|
|
23
24
|
export { captureTurnCheckpointContextState, restoreTurnCheckpointContextState };
|
|
@@ -198,7 +199,12 @@ export function recoverTurnCheckpoint(session) {
|
|
|
198
199
|
const current = Array.isArray(session.messages) ? session.messages : [];
|
|
199
200
|
const start = findTurnStart(current, checkpoint.currentUserContent);
|
|
200
201
|
const kept = start >= 0 ? current.slice(0, start) : current;
|
|
201
|
-
|
|
202
|
+
// The route switched models mid-turn (the marker says so) and the journal
|
|
203
|
+
// may predate the switch: its turn still carries the previous provider's
|
|
204
|
+
// native replay state, which the saved route's provider must not receive.
|
|
205
|
+
const turnMessages =
|
|
206
|
+
marker?.routeChanged === true ? stripProviderReplayMessages(finalized.messages) : finalized.messages;
|
|
207
|
+
session.messages = checkpoint.fullTranscript === true ? turnMessages : [...kept, ...turnMessages];
|
|
202
208
|
// Re-anchor provider usage / post-compact replacement state to the exact
|
|
203
209
|
// canonical checkpoint representation. If a legacy or corrupted checkpoint
|
|
204
210
|
// cannot prove alignment, retain the last provider reading for display but
|
|
@@ -558,7 +558,7 @@ export async function persistIterationMetrics(delta) {
|
|
|
558
558
|
// per model call of this session (every tool-loop iteration), replaced
|
|
559
559
|
// immutably so a consumer can tell by identity that it changed. Sessions
|
|
560
560
|
// saved before this field began at their first call after it.
|
|
561
|
-
session.usageReadout = foldUsageReadout(session.usageReadout, delta, delta.
|
|
561
|
+
session.usageReadout = foldUsageReadout(session.usageReadout, delta, delta.requestMs);
|
|
562
562
|
recordLastContextTokens(session, iterationContext(delta));
|
|
563
563
|
}
|
|
564
564
|
session.lastIterationIndex = iterationIndex;
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// use so a rename applies at once. Text shared across agents (tool schemas,
|
|
5
5
|
// other agents' shared prompt block) says "the representative agent" instead,
|
|
6
6
|
// so a rename never changes cached bytes.
|
|
7
|
-
import { join } from 'node:path';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
8
|
import { mixdogGlobalDir, readSafe } from '../context/skill-catalog.mjs';
|
|
9
|
+
import { mixyIdentityDir } from '../../../shared/agent-store.mjs';
|
|
9
10
|
|
|
10
11
|
export const DEFAULT_MIXY_NAME = 'Mixy';
|
|
11
12
|
|
|
@@ -17,7 +18,9 @@ export function mixyNameOf(manifest) {
|
|
|
17
18
|
|
|
18
19
|
export function mixyDisplayName() {
|
|
19
20
|
try {
|
|
20
|
-
|
|
21
|
+
// bots/mixy once Mixy is its own bot, agents/lead before (agent-store.mjs).
|
|
22
|
+
const dir = mixyIdentityDir(dirname(mixdogGlobalDir('agents')));
|
|
23
|
+
return mixyNameOf(JSON.parse(readSafe(join(dir, 'agent.json')) || '{}'));
|
|
21
24
|
} catch {
|
|
22
25
|
return DEFAULT_MIXY_NAME;
|
|
23
26
|
}
|
|
@@ -9,7 +9,7 @@ export interface RoutineProvenance {
|
|
|
9
9
|
export interface RoutineMessageMeta {
|
|
10
10
|
source: string;
|
|
11
11
|
routine: Record<string, unknown>;
|
|
12
|
-
transcript: { at: number; messageId?: string };
|
|
12
|
+
transcript: { at: number; messageId?: string; attachments?: unknown[] };
|
|
13
13
|
}
|
|
14
14
|
export interface RoutineMessage {
|
|
15
15
|
role: 'user';
|
|
@@ -22,6 +22,15 @@ export interface RoutineNotice {
|
|
|
22
22
|
routine: Record<string, unknown>;
|
|
23
23
|
/** Live run notices only (routineRunNotice). */
|
|
24
24
|
messageId?: string;
|
|
25
|
+
/** A task report's stored files (handoff-artifacts.mjs). */
|
|
26
|
+
attachments?: Array<{
|
|
27
|
+
id: string;
|
|
28
|
+
attachmentRef: string;
|
|
29
|
+
kind: string;
|
|
30
|
+
name: string;
|
|
31
|
+
mimeType: string;
|
|
32
|
+
size: number;
|
|
33
|
+
}>;
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
export const ROUTINE_RESULT_SOURCE: 'schedule-result';
|
|
@@ -45,7 +54,14 @@ export function agentTaskMessage(options: {
|
|
|
45
54
|
status: string;
|
|
46
55
|
text?: unknown;
|
|
47
56
|
taskSessionId: string;
|
|
57
|
+
/** 'session': any session other than a task session the agent created. */
|
|
58
|
+
target?: 'task' | 'session';
|
|
48
59
|
round?: number;
|
|
60
|
+
/** false: the report was posted without waking the agent (round-trip cap). */
|
|
61
|
+
woken?: boolean;
|
|
62
|
+
/** The files the turn produced (handoff-artifacts.mjs). */
|
|
63
|
+
files?: ReadonlyArray<{ name: string; path: string; mimeType?: string; size?: number; kind?: string; id?: string }>;
|
|
64
|
+
omitted?: number;
|
|
49
65
|
at?: number;
|
|
50
66
|
messageId?: string;
|
|
51
67
|
}): RoutineMessage;
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// close the boundary or forge a reminder. Every transcript renders the row as
|
|
10
10
|
// a notice: a routine result, never a user bubble or an assistant turn.
|
|
11
11
|
import { escapeHarnessTags } from '../../../shared/shell-output-summary.mjs';
|
|
12
|
+
import { artifactLines, artifactRecords, normalizeHandoffFiles } from '../../../shared/handoff-artifacts.mjs';
|
|
13
|
+
import { transcriptAttachmentFields } from '../../../shared/transcript-metadata.mjs';
|
|
12
14
|
|
|
13
15
|
export const ROUTINE_RESULT_SOURCE = 'schedule-result';
|
|
14
16
|
const SUMMARY_LIMIT = 600;
|
|
@@ -135,10 +137,28 @@ export const TASK_LOOP_CAP = 3;
|
|
|
135
137
|
* turn that ended `completed` / `failed` / `cancelled`. `round` counts the
|
|
136
138
|
* agent-initiated round trips; from TASK_LOOP_CAP on, the report asks the
|
|
137
139
|
* agent to check with the user before continuing. */
|
|
138
|
-
export function agentTaskMessage({
|
|
140
|
+
export function agentTaskMessage({
|
|
141
|
+
title,
|
|
142
|
+
status,
|
|
143
|
+
text = '',
|
|
144
|
+
taskSessionId,
|
|
145
|
+
target = 'task',
|
|
146
|
+
round = 0,
|
|
147
|
+
woken = true,
|
|
148
|
+
files = [],
|
|
149
|
+
omitted = 0,
|
|
150
|
+
at = Date.now(),
|
|
151
|
+
messageId,
|
|
152
|
+
}) {
|
|
139
153
|
const summary = routineSummary(text);
|
|
154
|
+
// The files the turn produced: listed for the agent, stored ones carried as
|
|
155
|
+
// the row's attachments (shown with the report, forwardable by id).
|
|
156
|
+
const produced = normalizeHandoffFiles(files);
|
|
157
|
+
const attachments = artifactRecords(produced);
|
|
140
158
|
const label = escapeHarnessTags(String(title).replace(/["\r\n]/g, ''));
|
|
141
|
-
|
|
159
|
+
// `target` 'session': any session other than a task session the agent created.
|
|
160
|
+
const session = target === 'session';
|
|
161
|
+
const head = `[mixdog-runtime] ${session ? 'Session' : 'Agent task session'} "${label}" (${taskSessionId})`;
|
|
142
162
|
let content;
|
|
143
163
|
if (status === 'started') content = `${head} started; its result is reported here when its turn ends.`;
|
|
144
164
|
else if (status === 'blocked') {
|
|
@@ -149,9 +169,13 @@ export function agentTaskMessage({ title, status, text = '', taskSessionId, roun
|
|
|
149
169
|
`<task-output session="${taskSessionId}" status="${status}">`,
|
|
150
170
|
escapeHarnessTags(summary),
|
|
151
171
|
'</task-output>',
|
|
172
|
+
...artifactLines(produced, omitted),
|
|
152
173
|
...(round >= TASK_LOOP_CAP
|
|
153
|
-
? [
|
|
174
|
+
? [
|
|
175
|
+
`This was round trip ${round} with this ${session ? 'session' : 'agent task session'}: check with the user before sending it more work.`,
|
|
176
|
+
]
|
|
154
177
|
: []),
|
|
178
|
+
...(woken ? [] : ['This report did not wake you: the round-trip cap with that agent thread is reached.']),
|
|
155
179
|
].join('\n');
|
|
156
180
|
}
|
|
157
181
|
return {
|
|
@@ -159,17 +183,33 @@ export function agentTaskMessage({ title, status, text = '', taskSessionId, roun
|
|
|
159
183
|
content,
|
|
160
184
|
meta: {
|
|
161
185
|
source: ROUTINE_RESULT_SOURCE,
|
|
162
|
-
routine: {
|
|
163
|
-
|
|
186
|
+
routine: {
|
|
187
|
+
kind: 'task',
|
|
188
|
+
name: title,
|
|
189
|
+
status,
|
|
190
|
+
summary,
|
|
191
|
+
sessionId: taskSessionId,
|
|
192
|
+
...(session ? { target: 'session' } : {}),
|
|
193
|
+
...(produced.length ? { files: produced } : {}),
|
|
194
|
+
},
|
|
195
|
+
transcript: { at, ...(messageId ? { messageId } : {}), ...(attachments.length ? { attachments } : {}) },
|
|
164
196
|
},
|
|
165
197
|
};
|
|
166
198
|
}
|
|
167
199
|
|
|
168
|
-
/** The hidden prompt that wakes the agent once its task results are posted
|
|
169
|
-
|
|
200
|
+
/** The hidden prompt that wakes the agent once its task results are posted;
|
|
201
|
+
* `more` counts posted reports it carries only as rows in the thread. */
|
|
202
|
+
export function agentTaskWakePrompt(messages, { more = 0 } = {}) {
|
|
203
|
+
const files = messages.some((message) => message?.meta?.routine?.files?.length);
|
|
170
204
|
return [
|
|
171
205
|
...messages.map((message) => message.content),
|
|
206
|
+
...(more > 0 ? [`${more} more report${more === 1 ? '' : 's'} arrived; read the rows above.`] : []),
|
|
172
207
|
'Relay the task result above to the user now, briefly and in your own words.',
|
|
208
|
+
...(files
|
|
209
|
+
? [
|
|
210
|
+
'Its files are already shown to the user with the report; name the ones that matter, link a path as a markdown link only when you refer to it, and pass them on to another session or agent with attachments.',
|
|
211
|
+
]
|
|
212
|
+
: []),
|
|
173
213
|
].join('\n\n');
|
|
174
214
|
}
|
|
175
215
|
|
|
@@ -218,11 +258,23 @@ export function routineResultNotice(message) {
|
|
|
218
258
|
}
|
|
219
259
|
if (routine.kind === 'task') {
|
|
220
260
|
const { name, status, summary, sessionId } = routine;
|
|
261
|
+
const session = routine.target === 'session';
|
|
221
262
|
const what = { started: 'started', blocked: 'waiting for confirmation' }[status] || status;
|
|
263
|
+
const files = normalizeHandoffFiles(routine.files);
|
|
222
264
|
return {
|
|
223
|
-
text: `🛠
|
|
265
|
+
text: `🛠 ${session ? 'Session' : 'Agent task'} ${what} · ${name}${summary ? ` — ${summary}` : ''}`,
|
|
224
266
|
tone: status === 'failed' ? 'warn' : 'info',
|
|
225
|
-
routine: {
|
|
267
|
+
routine: {
|
|
268
|
+
kind: 'task',
|
|
269
|
+
name,
|
|
270
|
+
status,
|
|
271
|
+
summary,
|
|
272
|
+
sessionId,
|
|
273
|
+
...(session ? { target: 'session' } : {}),
|
|
274
|
+
...(files.length ? { files } : {}),
|
|
275
|
+
},
|
|
276
|
+
// The stored files, in the field a prompt row draws its attachments from.
|
|
277
|
+
...transcriptAttachmentFields(message.meta.transcript),
|
|
226
278
|
};
|
|
227
279
|
}
|
|
228
280
|
const webhook = routine.kind === 'webhook';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { contextMeasurementStats } from '../../../../shared/context-measurement.mjs';
|
|
8
8
|
import { toolResultText } from './tool-result-text.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { createUserTextRestore } from './user-text.mjs';
|
|
10
10
|
import {
|
|
11
11
|
completionCardFromExecution,
|
|
12
12
|
parseModelVisibleCompletionWrapper,
|
|
@@ -379,7 +379,7 @@ function restoredNotificationToolItem(parsed, text, message, nextId, errorLabel)
|
|
|
379
379
|
};
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
function restoredUserTranscriptItems(message, nextId) {
|
|
382
|
+
function restoredUserTranscriptItems(message, nextId, userText) {
|
|
383
383
|
// Injected model-context payloads are model-visible but never user-authored:
|
|
384
384
|
// skill bodies (meta:'skill'), hook/system reminders, and tag-wrapped context
|
|
385
385
|
// blocks must not restore as user bubbles in any client.
|
|
@@ -403,7 +403,7 @@ function restoredUserTranscriptItems(message, nextId) {
|
|
|
403
403
|
const text = (typeof message.content === 'string' ? message.content : toolResultText(message.content)).trim();
|
|
404
404
|
return text ? [{ kind: 'user', id: nextId(), text, ...restoredTranscriptMetadata(message) }] : [];
|
|
405
405
|
}
|
|
406
|
-
const text =
|
|
406
|
+
const text = userText.text(message?.content).trim();
|
|
407
407
|
// Persisted async-completion wrappers ("Async shell task ... finished. Result:
|
|
408
408
|
// > ...") are the only durable record of a background completion — the live
|
|
409
409
|
// Response card is an event-time push that does not survive a transcript
|
|
@@ -434,7 +434,7 @@ function restoredUserTranscriptItems(message, nextId) {
|
|
|
434
434
|
}
|
|
435
435
|
const synthetic = parseSyntheticAgentMessage(text);
|
|
436
436
|
if (!synthetic) {
|
|
437
|
-
const imageRefs =
|
|
437
|
+
const imageRefs = userText.imageRefs(message?.content);
|
|
438
438
|
return [
|
|
439
439
|
{
|
|
440
440
|
kind: 'user',
|
|
@@ -442,6 +442,8 @@ function restoredUserTranscriptItems(message, nextId) {
|
|
|
442
442
|
text,
|
|
443
443
|
...restoredTranscriptMetadata(message),
|
|
444
444
|
...(imageRefs.length ? { imageRefs } : {}),
|
|
445
|
+
// Read by the model inside its running turn, as the live row says.
|
|
446
|
+
...(message?.meta?.source === 'steering' ? { steered: true } : {}),
|
|
445
447
|
},
|
|
446
448
|
];
|
|
447
449
|
}
|
|
@@ -474,7 +476,7 @@ export function workflowSwitchItem(id, boundary) {
|
|
|
474
476
|
};
|
|
475
477
|
}
|
|
476
478
|
|
|
477
|
-
function restoreTranscriptRange(messages, start, sessionId) {
|
|
479
|
+
function restoreTranscriptRange(messages, start, sessionId, userText) {
|
|
478
480
|
const items = [];
|
|
479
481
|
const pendingToolCalls = new Map();
|
|
480
482
|
for (let index = start; index < messages.length; index += 1) {
|
|
@@ -490,7 +492,7 @@ function restoreTranscriptRange(messages, start, sessionId) {
|
|
|
490
492
|
continue;
|
|
491
493
|
}
|
|
492
494
|
if (message?.role === 'user') {
|
|
493
|
-
items.push(...restoredUserTranscriptItems(message, restoredId));
|
|
495
|
+
items.push(...restoredUserTranscriptItems(message, restoredId, userText));
|
|
494
496
|
} else if (message?.role === 'assistant' && !isContextAckMessage(message, messages[index - 1])) {
|
|
495
497
|
items.push(...restoredAssistantTranscriptItems(message, restoredId));
|
|
496
498
|
items.push(...restoredToolCallItems(message, restoredId, pendingToolCalls));
|
|
@@ -533,9 +535,12 @@ function restoreTranscriptRange(messages, start, sessionId) {
|
|
|
533
535
|
|
|
534
536
|
export function restoreTranscriptItems(messages, { sessionId = 'session', itemLimit = Number.POSITIVE_INFINITY } = {}) {
|
|
535
537
|
const source = Array.isArray(messages) ? messages : [];
|
|
538
|
+
// One projection per load: a widened range reuses each prompt's parts and
|
|
539
|
+
// the paste-line reads share one budget.
|
|
540
|
+
const userText = createUserTextRestore();
|
|
536
541
|
const numericLimit = Number(itemLimit);
|
|
537
542
|
const limited = Number.isFinite(numericLimit) && numericLimit > 0;
|
|
538
|
-
if (!limited) return restoreTranscriptRange(source, 0, sessionId);
|
|
543
|
+
if (!limited) return restoreTranscriptRange(source, 0, sessionId, userText);
|
|
539
544
|
|
|
540
545
|
const limit = Math.max(1, Math.floor(numericLimit));
|
|
541
546
|
// Restore beyond the visible cap so a boundary tool run can merge exactly
|
|
@@ -549,7 +554,7 @@ export function restoreTranscriptItems(messages, { sessionId = 'session', itemLi
|
|
|
549
554
|
upperBound += restoredMessageItemUpperBound(source[start]);
|
|
550
555
|
}
|
|
551
556
|
|
|
552
|
-
let restored = restoreTranscriptRange(source, start, sessionId);
|
|
557
|
+
let restored = restoreTranscriptRange(source, start, sessionId, userText);
|
|
553
558
|
// Hidden/context messages and aggregate tool runs can make the cheap upper
|
|
554
559
|
// bound optimistic. Expand backward exponentially only when necessary.
|
|
555
560
|
let expansionTarget = Math.max(64, limit - restored.length);
|
|
@@ -559,7 +564,7 @@ export function restoreTranscriptItems(messages, { sessionId = 'session', itemLi
|
|
|
559
564
|
start -= 1;
|
|
560
565
|
expansion += restoredMessageItemUpperBound(source[start]);
|
|
561
566
|
}
|
|
562
|
-
restored = restoreTranscriptRange(source, start, sessionId);
|
|
567
|
+
restored = restoreTranscriptRange(source, start, sessionId, userText);
|
|
563
568
|
expansionTarget *= 2;
|
|
564
569
|
}
|
|
565
570
|
return restored.length > limit ? restored.slice(-limit) : restored;
|
|
@@ -116,14 +116,64 @@ function referencedFeedbackText(part) {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
const PASTED_LINES = /· (\d+) lines?$/;
|
|
120
|
+
const PASTED_TEXT_MAX_BYTES = 1024 * 1024;
|
|
121
|
+
/** Blob bytes one transcript load may read to count the lines of pastes
|
|
122
|
+
* whose stored name does not carry the count (pastes stored before it did). */
|
|
123
|
+
export const PASTED_LINE_READ_BUDGET_BYTES = 4 * 1024 * 1024;
|
|
124
|
+
|
|
125
|
+
/** Visible lines of a stored paste (as `pastedTextLineCount` counts them), or
|
|
126
|
+
* null when the count would take a read past the load's budget. */
|
|
127
|
+
function storedPasteLines(part, budget) {
|
|
128
|
+
const named = PASTED_LINES.exec(String(part.filename || ''));
|
|
129
|
+
if (named) return Number(named[1]);
|
|
130
|
+
const allowance = Math.min(PASTED_TEXT_MAX_BYTES, budget.remaining);
|
|
131
|
+
if (allowance <= 0) return null;
|
|
132
|
+
try {
|
|
133
|
+
const read = readAttachmentTextWithin(part, allowance);
|
|
134
|
+
if (read == null) return null;
|
|
135
|
+
budget.remaining -= Buffer.byteLength(read, 'utf8');
|
|
136
|
+
const value = read.replace(/\r\n?/g, '\n').replace(/\n+$/, '');
|
|
137
|
+
return value ? value.split('\n').length : 0;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// A blob that failed its integrity check was read before it failed: the
|
|
140
|
+
// read counts against the cap (at most the allowance). A missing blob
|
|
141
|
+
// read nothing.
|
|
142
|
+
if (error?.code !== 'ENOENT') budget.remaining -= allowance;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The chip token a stored pasted / attached text stood for in the live
|
|
148
|
+
* bubble (intake moved its body into the store and dropped the token). A
|
|
149
|
+
* paste whose line count is unknown shows as the plain `[Pasted text #n]`. */
|
|
150
|
+
function storedTextToken(part, number, budget) {
|
|
151
|
+
if (part?.type !== 'text') return '';
|
|
152
|
+
if (part.textKind === 'paste') {
|
|
153
|
+
const lines = storedPasteLines(part, budget);
|
|
154
|
+
return lines == null ? `[Pasted text #${number}]` : `[Pasted text #${number} +${lines} lines]`;
|
|
155
|
+
}
|
|
156
|
+
if (part.textKind === 'file') return `[File #${number}: ${part.filename || 'attachment.txt'}]`;
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
|
|
119
160
|
/** The display lines of a stored prompt's parts, and for each image part the
|
|
120
161
|
* line that draws it: the composer's metadata line right before it, else a
|
|
121
|
-
* bare line of its own (a bare line never stands for the next image).
|
|
122
|
-
|
|
162
|
+
* bare line of its own (a bare line never stands for the next image). A
|
|
163
|
+
* stored pasted or attached text shows as its chip token. */
|
|
164
|
+
function restoredUserParts(content, budget) {
|
|
123
165
|
const texts = [];
|
|
124
166
|
const images = [];
|
|
125
167
|
let pendingMetadata = '';
|
|
168
|
+
let storedTexts = 0;
|
|
126
169
|
for (const part of content) {
|
|
170
|
+
const token = isAttachmentReference(part) ? storedTextToken(part, storedTexts + 1, budget) : '';
|
|
171
|
+
if (token) {
|
|
172
|
+
storedTexts += 1;
|
|
173
|
+
texts.push(token);
|
|
174
|
+
pendingMetadata = '';
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
127
177
|
if (part?.type === 'image' || part?.type === 'input_image') {
|
|
128
178
|
let line = pendingMetadata;
|
|
129
179
|
if (!line) {
|
|
@@ -141,21 +191,48 @@ function restoredUserParts(content) {
|
|
|
141
191
|
return { texts, images };
|
|
142
192
|
}
|
|
143
193
|
|
|
194
|
+
/**
|
|
195
|
+
* The user-bubble projection of one transcript load: each stored prompt's
|
|
196
|
+
* parts are worked out once (its text and its image refs share them, and a
|
|
197
|
+
* load that widens its range sees the same messages again), and the blob
|
|
198
|
+
* reads that count paste lines share one byte budget.
|
|
199
|
+
*/
|
|
200
|
+
export function createUserTextRestore({ readBudgetBytes = PASTED_LINE_READ_BUDGET_BYTES } = {}) {
|
|
201
|
+
const budget = { remaining: readBudgetBytes };
|
|
202
|
+
const parts = new WeakMap();
|
|
203
|
+
const partsOf = (content) => {
|
|
204
|
+
let found = parts.get(content);
|
|
205
|
+
if (!found) {
|
|
206
|
+
found = restoredUserParts(content, budget);
|
|
207
|
+
parts.set(content, found);
|
|
208
|
+
}
|
|
209
|
+
return found;
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
text(content) {
|
|
213
|
+
if (!Array.isArray(content)) {
|
|
214
|
+
return withVisualizeFeedbackDisplay(typeof content === 'string' ? content : toolResultText(content));
|
|
215
|
+
}
|
|
216
|
+
return withVisualizeFeedbackDisplay(partsOf(content).texts.join('\n'));
|
|
217
|
+
},
|
|
218
|
+
/** The stored original of each image the restored bubble draws, by the
|
|
219
|
+
* `[Image: …]` line that draws it. Every image keeps its slot (one
|
|
220
|
+
* without a stored original has no `ref`) so later images do not shift
|
|
221
|
+
* onto it; empty when no image has a stored original. */
|
|
222
|
+
imageRefs(content) {
|
|
223
|
+
if (!Array.isArray(content)) return [];
|
|
224
|
+
const slots = partsOf(content).images.map(({ part, line }) =>
|
|
225
|
+
isAttachmentReference(part) ? { line, ref: part.attachmentRef } : { line }
|
|
226
|
+
);
|
|
227
|
+
return slots.some((slot) => slot.ref) ? slots : [];
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
144
232
|
export function restoredUserText(content) {
|
|
145
|
-
|
|
146
|
-
return withVisualizeFeedbackDisplay(typeof content === 'string' ? content : toolResultText(content));
|
|
147
|
-
}
|
|
148
|
-
return withVisualizeFeedbackDisplay(restoredUserParts(content).texts.join('\n'));
|
|
233
|
+
return createUserTextRestore().text(content);
|
|
149
234
|
}
|
|
150
235
|
|
|
151
|
-
/** The stored original of each image the restored bubble draws, by the
|
|
152
|
-
* `[Image: …]` line that draws it. Every image keeps its slot (one without a
|
|
153
|
-
* stored original has no `ref`) so later images do not shift onto it; empty
|
|
154
|
-
* when no image has a stored original. */
|
|
155
236
|
export function restoredUserImageRefs(content) {
|
|
156
|
-
|
|
157
|
-
const slots = restoredUserParts(content).images.map(({ part, line }) =>
|
|
158
|
-
isAttachmentReference(part) ? { line, ref: part.attachmentRef } : { line }
|
|
159
|
-
);
|
|
160
|
-
return slots.some((slot) => slot.ref) ? slots : [];
|
|
237
|
+
return createUserTextRestore().imageRefs(content);
|
|
161
238
|
}
|