mixdog 1.3.4 → 1.3.5
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/LICENSES/oh-my-pi-MIT.txt +23 -0
- package/NOTICE.md +15 -0
- package/README.md +11 -0
- package/package.json +2 -1
- package/scripts/embed-google-oauth-client.mjs +49 -0
- package/scripts/lib/test-spawn-ledger.mjs +46 -1
- package/scripts/test.mjs +5 -0
- package/src/defaults/shipped-history.json +8 -0
- package/src/defaults/skills/browser-use/SKILL.md +21 -9
- package/src/defaults/skills/setup/SKILL.md +7 -2
- package/src/defaults/skills/setup/references/actions.md +82 -27
- package/src/defaults/skills/setup/references/surfaces.md +67 -34
- package/src/defaults/skills/setup-guide/SKILL.md +3 -3
- package/src/lib/keychain-cjs.cjs +359 -29
- package/src/rules/agent-mode/AGENT_MODE.md +10 -2
- package/src/rules/agent-mode/MIXY.md +12 -0
- package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
- package/src/runtime/agent/orchestrator/config.mjs +18 -0
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
- package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
- package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
- package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
- package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
- package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
- package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
- package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
- package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
- package/src/runtime/channels/lib/scheduler.mjs +109 -14
- package/src/runtime/channels/lib/schedules-db.mjs +49 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
- package/src/runtime/channels/lib/worker-main.mjs +9 -0
- package/src/runtime/office/com/com-adapter.mjs +17 -1
- package/src/runtime/office/com/office-com-host.ps1 +51 -1
- package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
- package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
- package/src/runtime/office/pdf/document-preview.mjs +326 -8
- package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
- package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
- package/src/runtime/office/portable/portable-soffice.mjs +45 -5
- package/src/runtime/shared/bot-hops.mjs +62 -2
- package/src/runtime/shared/config.mjs +49 -22
- package/src/runtime/shared/connector-catalog.d.mts +4 -2
- package/src/runtime/shared/connector-catalog.mjs +16 -8
- package/src/runtime/shared/developer-options.mjs +32 -1
- package/src/runtime/shared/google-oauth-client.generated.json +4 -0
- package/src/runtime/shared/google-oauth-client.mjs +34 -9
- package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
- package/src/runtime/shared/llm/well-formed.mjs +66 -0
- package/src/runtime/shared/provider-accounts.mjs +224 -58
- package/src/runtime/shared/provider-api-key.mjs +19 -8
- package/src/runtime/shared/routine-trigger.mjs +42 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -0
- package/src/runtime/shared/transcript-metadata.mjs +38 -0
- package/src/runtime/shared/user-data-guard.mjs +30 -2
- package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
- package/src/session-runtime/agent-home-delivery.mjs +57 -13
- package/src/session-runtime/agent-room-delivery.mjs +28 -1
- package/src/session-runtime/agent-task-sessions.mjs +292 -79
- package/src/session-runtime/boot/begin.mjs +3 -2
- package/src/session-runtime/boot/tools.mjs +2 -0
- package/src/session-runtime/bot-chain.mjs +32 -0
- package/src/session-runtime/bot-status.mjs +7 -4
- package/src/session-runtime/channel-config-api.mjs +63 -17
- package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
- package/src/session-runtime/internal-tool-executor.mjs +4 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
- package/src/session-runtime/mixy-heartbeat.mjs +807 -0
- package/src/session-runtime/model-route/route-tuning.mjs +10 -6
- package/src/session-runtime/owned-automation-run.mjs +33 -12
- package/src/session-runtime/provider-auth/login.mjs +7 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
- package/src/session-runtime/routine-home-turn.mjs +33 -6
- package/src/session-runtime/schedule-session-run.mjs +7 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
- package/src/session-runtime/services/channel-admin.mjs +66 -18
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +128 -25
- package/src/session-runtime/services/schedule-access.mjs +14 -0
- package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
- package/src/session-runtime/session-create/session-options.mjs +7 -4
- package/src/session-runtime/settings-system-api.mjs +16 -1
- package/src/session-runtime/setup-tool/executor.mjs +49 -14
- package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
- package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
- package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
- package/src/session-runtime/task-session-route.mjs +99 -0
- package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
- package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
- package/src/session-runtime/tool-surface.mjs +4 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
- package/src/session-runtime/workspace/transitions.mjs +10 -2
- package/src/standalone/daemon-session-control.mjs +88 -20
- package/src/standalone/daemon.mjs +8 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
- package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
- package/src/standalone/session-service.mjs +13 -0
- package/src/standalone/session-tool-output.mjs +8 -5
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/dist/index.mjs +24 -2
- package/src/tui/session/notice-surface.mjs +17 -1
- package/src/tui/session/session-api/integrations.mjs +1 -0
- package/src/tui/session/session-api/lifecycle.mjs +8 -0
- package/src/tui/session/session-api/settings.mjs +14 -2
- package/src/tui/session/turn-stream.mjs +17 -1
- package/src/tui/session/turn.mjs +22 -2
|
@@ -14,17 +14,22 @@
|
|
|
14
14
|
// allowed, payments and permanent deletes denied. Group-room members register
|
|
15
15
|
// a port here (session-runtime/held-requests.mjs) that turns their asks into
|
|
16
16
|
// held requests when no approval card is attached.
|
|
17
|
+
// The "Allow all actions" developer option (shared/developer-options.mjs)
|
|
18
|
+
// skips every ask below, the bot's per-action `ask` mode and a hook `ask`
|
|
19
|
+
// included, except a payment or permanent delete; a `block` and a hook `deny`
|
|
20
|
+
// still deny.
|
|
17
21
|
import { MIXY_AGENT_ID, agentIdOfHomeSession } from '../agent-home.mjs';
|
|
18
22
|
import { serverConnector } from '../../mcp/server-connectors.mjs';
|
|
19
23
|
import { readAgentDefinitionKind } from '../../../../shared/agent-kind.mjs';
|
|
20
24
|
import { isContactAgentId } from '../../../../shared/agent-route-config.mjs';
|
|
25
|
+
import { developerOptionEnabledStrict } from '../../../../shared/developer-options.mjs';
|
|
21
26
|
import { mixdogRoot, resolvePluginData } from '../../../../shared/plugin-paths.mjs';
|
|
22
27
|
import { roomMemberOfSession } from '../agent-room.mjs';
|
|
23
28
|
import { agentDisplayName } from '../../context/role-instructions.mjs';
|
|
24
29
|
import { delegationOf } from './delegation.mjs';
|
|
25
30
|
import { setUnattendedApprovalPort, unattendedApprovalPort } from './unattended-port.mjs';
|
|
26
|
-
import { classifyAsDelete, classifyOutboundCall, isRememberableAction } from './outbound-policy.mjs';
|
|
27
|
-
import { humanGateStore } from './store.mjs';
|
|
31
|
+
import { classifyAsDelete, classifyOutboundCall, isAlwaysAskAction, isRememberableAction } from './outbound-policy.mjs';
|
|
32
|
+
import { DEV_ALLOW_ALL_DECISION, humanGateStore } from './store.mjs';
|
|
28
33
|
import {
|
|
29
34
|
AUTOMATION_GRANT_REASON,
|
|
30
35
|
RUN_FORBIDDEN_REASON,
|
|
@@ -33,7 +38,7 @@ import {
|
|
|
33
38
|
savesAutomation,
|
|
34
39
|
} from './run-policy.mjs';
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
const OUTBOUND_APPROVAL_REASON = 'This sends or posts outside Mixdog; it needs your approval.';
|
|
37
42
|
export const BLOCKED_ACTION_REASON = "This agent's permission blocks this action.";
|
|
38
43
|
/** Outbound grants may be remembered for this agent, never for every agent;
|
|
39
44
|
* payments and deletes never. */
|
|
@@ -105,6 +110,22 @@ export function approvalSurface(sessionId) {
|
|
|
105
110
|
|
|
106
111
|
const actionOf = (decision) => String(decision?.action || decision?.decision || '').toLowerCase();
|
|
107
112
|
|
|
113
|
+
/** Whether the "Allow all actions" developer option is on (default off).
|
|
114
|
+
* Fail-closed: an unreadable, missing or malformed config is off (the call
|
|
115
|
+
* asks), and a value restored from a backup never counts. */
|
|
116
|
+
export const allowAllActionsEnabled = () => developerOptionEnabledStrict('allowAllActions');
|
|
117
|
+
|
|
118
|
+
/** Whether the option lets a call of this classification (or none) run without
|
|
119
|
+
* a card: every ask, never a classified payment or permanent delete. */
|
|
120
|
+
const skipsApproval = (classified) => !isAlwaysAskAction(classified?.category) && allowAllActionsEnabled();
|
|
121
|
+
|
|
122
|
+
/** The hook decision once its card is skipped: an `ask` runs as an allow,
|
|
123
|
+
* keeping any rewrite it carried. */
|
|
124
|
+
function withoutAsk(decision) {
|
|
125
|
+
if (actionOf(decision) !== 'ask') return decision;
|
|
126
|
+
return { ...decision, action: decision.name || decision.args ? 'modify' : 'allow' };
|
|
127
|
+
}
|
|
128
|
+
|
|
108
129
|
async function mcpAnnotations(name, scopeId) {
|
|
109
130
|
if (!String(name).startsWith('mcp__')) return undefined;
|
|
110
131
|
try {
|
|
@@ -185,11 +206,17 @@ export async function humanGateDecision(call, decision, ctx = {}) {
|
|
|
185
206
|
const outbound = classified && actingBotId(ctx.sessionId) ? classified : null;
|
|
186
207
|
if (!outbound) {
|
|
187
208
|
if (action !== 'ask') return { decision, request: null, gate: null };
|
|
209
|
+
const gate = { agentId, kind: 'approval', tool: String(call?.name || ''), target: '' };
|
|
210
|
+
if (skipsApproval(classified)) {
|
|
211
|
+
const store = ctx.store || (await humanGateStore());
|
|
212
|
+
await store.recordAudit({ ...gate, decision: DEV_ALLOW_ALL_DECISION, device: '' });
|
|
213
|
+
return { decision: withoutAsk(decision), request: null, gate: null };
|
|
214
|
+
}
|
|
188
215
|
return {
|
|
189
216
|
// Saving a schedule or webhook says plainly what the approval grants.
|
|
190
217
|
decision: savesAutomation(call) ? { ...decision, reason: AUTOMATION_GRANT_REASON } : decision,
|
|
191
218
|
request: { kind: 'approval', agentId, remember: ['once'] },
|
|
192
|
-
gate
|
|
219
|
+
gate,
|
|
193
220
|
};
|
|
194
221
|
}
|
|
195
222
|
const gate = { agentId, kind: 'outbound', action: outbound.category, tool: outbound.tool, target: outbound.target };
|
|
@@ -215,6 +242,11 @@ export async function humanGateDecision(call, decision, ctx = {}) {
|
|
|
215
242
|
await store.recordAudit({ ...gate, decision: 'blocked', device: '' });
|
|
216
243
|
return { decision: { ...base, action: 'deny', reason: BLOCKED_ACTION_REASON }, request: null, gate: null };
|
|
217
244
|
}
|
|
245
|
+
// The bot's `ask` mode (and a hook's `ask`) is bypassed; a payment or delete is not.
|
|
246
|
+
if (skipsApproval(outbound)) {
|
|
247
|
+
await store.recordAudit({ ...gate, decision: DEV_ALLOW_ALL_DECISION, device: '' });
|
|
248
|
+
return { decision: withoutAsk(decision), request: null, gate: null };
|
|
249
|
+
}
|
|
218
250
|
if (action !== 'ask' && mode === 'allow') {
|
|
219
251
|
await store.recordAudit({ ...gate, decision: 'remembered', device: '' });
|
|
220
252
|
// The remembered grant answers this call: no later layer asks it again.
|
|
@@ -39,6 +39,12 @@ export function isRememberableAction(kind) {
|
|
|
39
39
|
return ACTION_KINDS.includes(kind) && !UNREMEMBERED_ACTIONS.has(kind);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Whether `kind` is a payment or permanent delete: it asks every time,
|
|
43
|
+
* whatever else is allowed (also by the "Allow all actions" developer option). */
|
|
44
|
+
export function isAlwaysAskAction(kind) {
|
|
45
|
+
return UNREMEMBERED_ACTIONS.has(kind);
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
const GITHUB_OUTBOUND = new Map([
|
|
43
49
|
['issue.comment', 'comment'],
|
|
44
50
|
['pr.comment', 'comment'],
|
|
@@ -17,7 +17,7 @@ import { ACTION_KINDS, isRememberableAction } from './outbound-policy.mjs';
|
|
|
17
17
|
const ANY_AGENT = '*';
|
|
18
18
|
const FIELD_LIMIT = 200;
|
|
19
19
|
const AUDIT_KEYS = ['agentId', 'kind', 'action', 'tool', 'target', 'decision', 'device'];
|
|
20
|
-
|
|
20
|
+
const PERMISSION_MODES = Object.freeze(['ask', 'allow', 'block']);
|
|
21
21
|
|
|
22
22
|
const field = (value) => String(value ?? '').slice(0, FIELD_LIMIT);
|
|
23
23
|
const grantId = (agentId, action) =>
|
|
@@ -173,12 +173,24 @@ export async function setAgentPermission(agentId, action, mode, options) {
|
|
|
173
173
|
return store.permissions(agent);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
/** The audit decision of a call the "Allow all actions" developer option let through unasked. */
|
|
177
|
+
export const DEV_ALLOW_ALL_DECISION = 'dev-allow-all';
|
|
178
|
+
|
|
179
|
+
const BROWSER_AUDIT_DECISIONS = Object.freeze([
|
|
180
|
+
'approve',
|
|
181
|
+
'deny',
|
|
182
|
+
'granted',
|
|
183
|
+
'remembered',
|
|
184
|
+
'run-allowed',
|
|
185
|
+
'blocked',
|
|
186
|
+
DEV_ALLOW_ALL_DECISION,
|
|
187
|
+
]);
|
|
177
188
|
|
|
178
189
|
/** Audit one Browser Use approval of a bot (capability recordAgentApprovalAudit):
|
|
179
190
|
* the bot, the action kind and what happened — `approve` / `deny` for the card,
|
|
180
191
|
* `granted` when "always allow" was saved, `remembered` when a saved allow let
|
|
181
|
-
* a step run unasked
|
|
192
|
+
* a step run unasked, `dev-allow-all` when the developer option did. Never a
|
|
193
|
+
* page address, target or value. */
|
|
182
194
|
export async function recordAgentApprovalAudit(agentId, action, decision) {
|
|
183
195
|
const agent = requiredAgentId(agentId);
|
|
184
196
|
if (!ACTION_KINDS.includes(action)) throw new Error(`action must be one of ${ACTION_KINDS.join(', ')}`);
|
|
@@ -62,6 +62,12 @@ function assistantEchoFields(payloads) {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// The tail reminder the response was generated under (send-with-recovery.mjs),
|
|
66
|
+
// kept verbatim so a turn-scoped provider replays it byte-identically.
|
|
67
|
+
export function turnReminderField(resp) {
|
|
68
|
+
return typeof resp?.turnReminder === 'string' && resp.turnReminder ? { turnReminder: resp.turnReminder } : {};
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
// Intermediate (non-tool) assistant turn: max-output partials, refusal
|
|
66
72
|
// narration, provider continuations, and the terminal turn committed before a
|
|
67
73
|
// steering drain. Returns null when the turn carries nothing committable — a
|
|
@@ -89,6 +95,7 @@ export function buildIntermediateAssistantMessage(resp, opts) {
|
|
|
89
95
|
role: 'assistant',
|
|
90
96
|
content,
|
|
91
97
|
...assistantEchoFields(payloads),
|
|
98
|
+
...turnReminderField(resp),
|
|
92
99
|
...(stopReason ? { stopReason } : {}),
|
|
93
100
|
...(terminationReason ? { terminationReason } : {}),
|
|
94
101
|
},
|
|
@@ -108,6 +115,7 @@ export function buildToolCallAssistantMessage(response, { calls, opts }) {
|
|
|
108
115
|
content: response.content || '',
|
|
109
116
|
toolCalls: compactToolCallsForHistory(calls, { deferBodies: true }),
|
|
110
117
|
...assistantEchoFields(assistantEchoPayloads(response, { keepAssistantBlocks: true })),
|
|
118
|
+
...turnReminderField(response),
|
|
111
119
|
},
|
|
112
120
|
opts
|
|
113
121
|
);
|
|
@@ -55,6 +55,9 @@ export function createLoopState({ provider, messages, model, tools, cwd, sendOpt
|
|
|
55
55
|
cwd: resolveLiveToolCwd(cwd, sessionRef),
|
|
56
56
|
iterations: 0,
|
|
57
57
|
toolCallsTotal: 0,
|
|
58
|
+
// A capped turn (turn-limits.mjs): told to reply, and ended by calling tools anyway.
|
|
59
|
+
turnLimitNoticed: false,
|
|
60
|
+
turnLimitEnded: false,
|
|
58
61
|
lastUsage: undefined,
|
|
59
62
|
firstTurnUsage: undefined,
|
|
60
63
|
response: undefined,
|
|
@@ -147,6 +150,9 @@ export function createLoopState({ provider, messages, model, tools, cwd, sendOpt
|
|
|
147
150
|
* promote an empty/abnormal finish to an explicit Lead-facing error). */
|
|
148
151
|
export function finishLoop(state) {
|
|
149
152
|
const { response } = state;
|
|
153
|
+
// A reaction-only turn is a successful completion, never an empty finish.
|
|
154
|
+
const terminationReason = classifyTerminationReason(response, { sessionAgent: state.sessionAgent });
|
|
155
|
+
const reactionCompleted = terminationReason === 'empty' && state.noToolTurn.reactionCompleted;
|
|
150
156
|
return {
|
|
151
157
|
...response,
|
|
152
158
|
usage: state.lastUsage || response.usage,
|
|
@@ -157,7 +163,8 @@ export function finishLoop(state) {
|
|
|
157
163
|
toolCallsTotal: state.toolCallsTotal,
|
|
158
164
|
providerState: state.providerState,
|
|
159
165
|
providerStateUpdated: state.providerStateUpdated,
|
|
160
|
-
terminationReason:
|
|
166
|
+
terminationReason: reactionCompleted ? undefined : terminationReason,
|
|
167
|
+
...(reactionCompleted ? { reactionCompleted: true } : {}),
|
|
161
168
|
providerContinuations: state.noToolTurn.providerContinuations,
|
|
162
169
|
...(state.routeUsageHops?.length
|
|
163
170
|
? {
|
|
@@ -20,8 +20,11 @@ export function createTurnSegments({ messages, opts }) {
|
|
|
20
20
|
// Record a mid-turn segment for the caller-facing aggregate and surface it
|
|
21
21
|
// live for every session.
|
|
22
22
|
// `committed: true` marks text that is the just-committed message's own.
|
|
23
|
+
// A committed message is whole, so the text after it is another message
|
|
24
|
+
// and a break keeps the aggregate from running the two together; a part
|
|
25
|
+
// that was cut off (max-output recovery) resumes right where it stopped.
|
|
23
26
|
record(text, { committed = false } = {}) {
|
|
24
|
-
parts.push(text);
|
|
27
|
+
parts.push(committed ? `${text.trimEnd()}\n\n` : text);
|
|
25
28
|
try {
|
|
26
29
|
opts.onAssistantText?.(text, { committed });
|
|
27
30
|
} catch {
|
|
@@ -18,6 +18,9 @@ import { resetOutputLimitRecovery, resolveOutputLimit } from './no-tool-turn/out
|
|
|
18
18
|
import { resolveRefusal } from './no-tool-turn/refusal.mjs';
|
|
19
19
|
import { resumeContinuation } from './no-tool-turn/continuation.mjs';
|
|
20
20
|
import { nudgeEmptyTurn } from './no-tool-turn/empty-nudge.mjs';
|
|
21
|
+
import { isReactionSuccess, reactionCallIds, takeReactionTarget } from '../../runtime-core/reaction-tool-defs.mjs';
|
|
22
|
+
import { messageIdOf } from '../message-ids.mjs';
|
|
23
|
+
import { isReactableMessage } from '../message-reactions.mjs';
|
|
21
24
|
|
|
22
25
|
// `escalateMaxTokens()` asks the live provider for a higher one-shot max_tokens
|
|
23
26
|
// (number) or reports none (null/undefined) — the loop stays provider-agnostic.
|
|
@@ -37,6 +40,9 @@ export function createNoToolTurnResolver({
|
|
|
37
40
|
outputEscalationUsed: false,
|
|
38
41
|
refusalRetryUsed: false,
|
|
39
42
|
emptyNudgeStreak: 0,
|
|
43
|
+
// Successful reactions (react tool): the input each was left against and
|
|
44
|
+
// whether it answered that input.
|
|
45
|
+
reactions: [],
|
|
40
46
|
// Structured provider continuation signals honored this turn. Diagnostic
|
|
41
47
|
// only: the loop has no iteration cap, so this count bounds nothing. A
|
|
42
48
|
// text-only continuation runaway is bounded by continuationsSinceToolBatch
|
|
@@ -46,6 +52,39 @@ export function createNoToolTurnResolver({
|
|
|
46
52
|
// continuation runaway (see PROVIDER_CONTINUATION_NO_TOOL_LIMIT).
|
|
47
53
|
continuationsSinceToolBatch: 0,
|
|
48
54
|
};
|
|
55
|
+
// The stable id (message-ids.mjs, kept across compaction) of the newest
|
|
56
|
+
// real user input; runtime reminders and notices are not reactable.
|
|
57
|
+
// A group-room member turn's prompt is a runtime message (not reactable)
|
|
58
|
+
// that carries the newest room-log user row it shows (`roomRow`).
|
|
59
|
+
const roomRowOf = (message) => message?.meta?.transcript?.roomRow || '';
|
|
60
|
+
const latestInput = () => messages.findLast((message) => isReactableMessage(message) || roomRowOf(message));
|
|
61
|
+
const latestInputId = () => messageIdOf(latestInput());
|
|
62
|
+
// A reaction exempts the input only while no newer input arrived since it
|
|
63
|
+
// landed (same latest input id) and it answered that input.
|
|
64
|
+
const reactionAnswersLatest = () => {
|
|
65
|
+
const id = latestInputId();
|
|
66
|
+
return state.reactions.some((reaction) => reaction.answered && reaction.inputId === id);
|
|
67
|
+
};
|
|
68
|
+
// What this batch's react calls hit, from the id the react tool recorded
|
|
69
|
+
// for each (never rebuilt from this transcript: a room reaction runs
|
|
70
|
+
// against the room's own log). Every record is consumed; a success that
|
|
71
|
+
// recorded none exempts nothing. A home-chat reaction answers the input
|
|
72
|
+
// only when it hit that very message; a room reaction, whose message is in
|
|
73
|
+
// another log, only when it hit the row this turn's prompt showed.
|
|
74
|
+
const batchReactions = (calls, results) => {
|
|
75
|
+
const input = latestInput();
|
|
76
|
+
const inputId = messageIdOf(input);
|
|
77
|
+
const hit = [];
|
|
78
|
+
for (const id of reactionCallIds(calls)) {
|
|
79
|
+
const target = takeReactionTarget(sessionId, id);
|
|
80
|
+
if (!target || !results.some((message) => isReactionSuccess(message, new Set([id])))) continue;
|
|
81
|
+
const answered = target.room
|
|
82
|
+
? roomRowOf(input) === target.messageId
|
|
83
|
+
: inputId !== '' && target.messageId === inputId;
|
|
84
|
+
hit.push({ inputId, answered });
|
|
85
|
+
}
|
|
86
|
+
return hit;
|
|
87
|
+
};
|
|
49
88
|
const trace = (iteration, kind, payload) => {
|
|
50
89
|
try {
|
|
51
90
|
appendAgentTrace({ sessionId, iteration, kind, payload, agent: sessionAgent || null });
|
|
@@ -101,7 +140,9 @@ export function createNoToolTurnResolver({
|
|
|
101
140
|
// output, …) and a text-only terminal turn is the correct shape —
|
|
102
141
|
// nudging them produces a contradictory user message that traps the
|
|
103
142
|
// model in a tool-call-blocked vs contract-required oscillation.
|
|
104
|
-
|
|
143
|
+
// A turn that left a reaction on the user's message may end with no
|
|
144
|
+
// text: the reaction is its answer (react tool), never an empty turn.
|
|
145
|
+
if (!hasContent && !HIDDEN_AGENT_NAMES.has(sessionAgent) && !reactionAnswersLatest()) {
|
|
105
146
|
return nudgeEmptyTurn(
|
|
106
147
|
response,
|
|
107
148
|
{ stopReason, isIncompleteStop: stopReason && INCOMPLETE_STOP_REASONS.has(stopReason) },
|
|
@@ -132,9 +173,16 @@ export function createNoToolTurnResolver({
|
|
|
132
173
|
state.emptyNudgeStreak = 0;
|
|
133
174
|
resetOutputLimitRecovery(state);
|
|
134
175
|
},
|
|
135
|
-
|
|
176
|
+
// `results` are the batch's tool result messages: only a react call that
|
|
177
|
+
// succeeded marks the turn as having reacted.
|
|
178
|
+
noteToolBatchCompleted(calls = [], results = []) {
|
|
136
179
|
state.continuationsSinceToolBatch = 0;
|
|
137
180
|
segments.clear();
|
|
181
|
+
state.reactions.push(...batchReactions(calls, results));
|
|
182
|
+
},
|
|
183
|
+
// The loop ended on a reaction that answered the latest user input.
|
|
184
|
+
get reactionCompleted() {
|
|
185
|
+
return reactionAnswersLatest();
|
|
138
186
|
},
|
|
139
187
|
get providerContinuations() {
|
|
140
188
|
return state.providerContinuationCount;
|
|
@@ -96,6 +96,9 @@ export async function beginIteration(state) {
|
|
|
96
96
|
opts.providerState = state.providerState;
|
|
97
97
|
if (state.forcedFirstTool && state.toolCallsTotal === 0) {
|
|
98
98
|
opts.toolChoice = 'required';
|
|
99
|
+
} else if (state.turnLimitNoticed) {
|
|
100
|
+
// A capped turn told to reply may only answer (turn-limits.mjs).
|
|
101
|
+
opts.toolChoice = 'none';
|
|
99
102
|
} else {
|
|
100
103
|
delete opts.toolChoice;
|
|
101
104
|
}
|
|
@@ -7,7 +7,13 @@ import { runAbortable, throwIfAborted } from '../../../../../shared/abort-race.m
|
|
|
7
7
|
import { evaluateExecPolicy } from '../../../tools/bash-policy-scan.mjs';
|
|
8
8
|
import { beginSessionApprovalWait } from '../../manager/runtime-liveness.mjs';
|
|
9
9
|
import { agentMcpToolPermission, writeAgentMcpToolPermission } from '../../../mcp/agent-connectors.mjs';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
actingBotId,
|
|
12
|
+
actingContactBotId,
|
|
13
|
+
allowAllActionsEnabled,
|
|
14
|
+
humanGateDecision,
|
|
15
|
+
recordHumanDecision,
|
|
16
|
+
} from '../../human-gate/gate.mjs';
|
|
11
17
|
import { applyEditedArgs } from '../../human-gate/requests.mjs';
|
|
12
18
|
|
|
13
19
|
const isArgsObject = (value) => value && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -62,13 +68,16 @@ export const CONNECTOR_APPROVAL_REASON = 'This connector tool is set to ask befo
|
|
|
62
68
|
* coding Lead, a task session, a workflow agent, a sub-agent) runs it unasked.
|
|
63
69
|
* Run after the hooks, on the final call: a renamed call is judged under its
|
|
64
70
|
* final name. A call the outbound gate owns (its card approved it, or a
|
|
65
|
-
* remembered outbound grant answered it) is not asked a second time.
|
|
71
|
+
* remembered outbound grant answered it) is not asked a second time. With the
|
|
72
|
+
* "Allow all actions" developer option on, `needs_approval` is not asked
|
|
73
|
+
* either (a payment or permanent delete was asked by the outbound gate above).
|
|
66
74
|
* @returns {Promise<{ denial: string } | null>} the denial, or null to run
|
|
67
75
|
*/
|
|
68
76
|
export function applyConnectorApproval(call, { agentId, callerSessionId, executeOpts, toolApprovalHook }) {
|
|
69
77
|
if (call.outboundApproved) return null;
|
|
70
78
|
if (!actingBotId(callerSessionId)) return null;
|
|
71
79
|
if (agentMcpToolPermission(agentId, call.name) !== 'needs_approval') return null;
|
|
80
|
+
if (allowAllActionsEnabled()) return null;
|
|
72
81
|
// "Always allow" is offered only to the contact bot that is acting, for its
|
|
73
82
|
// own policy entry for this one tool: never another bot or Mixy.
|
|
74
83
|
const botId = actingContactBotId(callerSessionId);
|
|
@@ -9,6 +9,7 @@ import { processToolBatch } from '../tool-batch.mjs';
|
|
|
9
9
|
import { CROSS_TURN_CAP, REPEAT_FAIL_LIMIT } from './loop-state.mjs';
|
|
10
10
|
import { markStepEffort } from './step-auto-effort.mjs';
|
|
11
11
|
import { resolveExecutionToolCalls } from './tool-call-resolution.mjs';
|
|
12
|
+
import { callsAllowed, settleTurnLimits, turnLimitsOf } from './turn-limits.mjs';
|
|
12
13
|
|
|
13
14
|
function isSleepLikeToolCall(call) {
|
|
14
15
|
const name = String(call?.name || call?.toolName || call?.function?.name || '').toLowerCase();
|
|
@@ -17,9 +18,13 @@ function isSleepLikeToolCall(call) {
|
|
|
17
18
|
|
|
18
19
|
export async function runToolPhase(state, round, sent, onToolCall) {
|
|
19
20
|
const { opts, messages, tools, sessionId, sessionRef, signal, response } = state;
|
|
20
|
-
state.noToolTurn.noteToolCallTurn();
|
|
21
21
|
const calls = response.toolCalls;
|
|
22
|
+
state.noToolTurn.noteToolCallTurn();
|
|
22
23
|
round.resolveToolCallId.all(calls);
|
|
24
|
+
// A capped turn (turn-limits.mjs): calls past its cap are answered unrun,
|
|
25
|
+
// and a batch after it was told to reply ends it.
|
|
26
|
+
const allowed = callsAllowed(state);
|
|
27
|
+
const overrun = state.turnLimitNoticed === true;
|
|
23
28
|
state.toolCallsTotal += calls.length;
|
|
24
29
|
// Surface any mid-turn assistant text (preamble before a tool call) to the
|
|
25
30
|
// UI. Providers that stream via onTextDelta already rendered it; providers
|
|
@@ -44,6 +49,7 @@ export async function runToolPhase(state, round, sent, onToolCall) {
|
|
|
44
49
|
opts.onToolPhaseStarted?.();
|
|
45
50
|
} catch {}
|
|
46
51
|
const toolsT0 = Date.now();
|
|
52
|
+
const resultsFrom = messages.length;
|
|
47
53
|
({ dedupStubTotal: state.dedupStubTotal, editCount: state.editCount } = await processToolBatch({
|
|
48
54
|
calls,
|
|
49
55
|
messages,
|
|
@@ -67,10 +73,13 @@ export async function runToolPhase(state, round, sent, onToolCall) {
|
|
|
67
73
|
pushToolResultMessage: state.pushToolResultMessage,
|
|
68
74
|
throwIfAborted: state.throwIfAborted,
|
|
69
75
|
repeatFailLimit: REPEAT_FAIL_LIMIT,
|
|
76
|
+
callsAllowed: allowed,
|
|
77
|
+
wrapUpAt: turnLimitsOf(state.opts)?.wrapUpAt,
|
|
70
78
|
}));
|
|
71
79
|
// A turn recovered from a stream cut that dropped an in-flight tool call
|
|
72
80
|
// carries a runtime notice for the model, delivered after its results.
|
|
73
81
|
if (response.recoveryNotice) messages.push(response.recoveryNotice);
|
|
82
|
+
settleTurnLimits(state, overrun);
|
|
74
83
|
const toolsEndedAt = Date.now();
|
|
75
84
|
// Starts the step judgment; the next request uses it if it has resolved by
|
|
76
85
|
// then (send-phase.mjs applyStepEffort), else the one after.
|
|
@@ -91,6 +100,6 @@ export async function runToolPhase(state, round, sent, onToolCall) {
|
|
|
91
100
|
});
|
|
92
101
|
state.lastToolBatchEndedAt = toolsEndedAt;
|
|
93
102
|
state.toolBatchJustCompleted = true;
|
|
94
|
-
state.noToolTurn.noteToolBatchCompleted();
|
|
103
|
+
state.noToolTurn.noteToolBatchCompleted(calls, messages.slice(resultsFrom));
|
|
95
104
|
state.lastToolBatchHadSleep = calls.some(isSleepLikeToolCall);
|
|
96
105
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turn-limits.mjs — a turn's caps (sendOpts.turnLimits; a heartbeat turn's,
|
|
3
|
+
* manager/ask-turn-loop-options.mjs): `maxToolCalls` tool calls and
|
|
4
|
+
* `wrapUpAt` (epoch ms). A call past the cap, or one made after the wrap-up
|
|
5
|
+
* time, is answered without running. Once a cap is reached the model is told
|
|
6
|
+
* to reply, and every later request of the turn forbids tool use
|
|
7
|
+
* (toolChoice 'none'). A response that still calls tools after that ends the
|
|
8
|
+
* turn with no reply, its calls answered unrun. A capped turn runs no call
|
|
9
|
+
* eagerly (eager-dispatch.mjs), so none past the cap starts while streaming.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const NOTICE =
|
|
13
|
+
'[mixdog-runtime] This turn has reached its tool-call or time limit: no further tool call runs. Reply now with your final answer from what you already have.';
|
|
14
|
+
const REFUSAL =
|
|
15
|
+
'[turn-limit] This turn has reached its tool-call or time limit; this call was not run. Reply now without tools.';
|
|
16
|
+
|
|
17
|
+
/** `{ maxToolCalls, wrapUpAt }` of a capped turn (Infinity where unset), else null. */
|
|
18
|
+
export function turnLimitsOf(opts) {
|
|
19
|
+
const limits = opts?.turnLimits;
|
|
20
|
+
if (!limits || typeof limits !== 'object') return null;
|
|
21
|
+
return {
|
|
22
|
+
maxToolCalls: Number.isInteger(limits.maxToolCalls) && limits.maxToolCalls >= 0 ? limits.maxToolCalls : Infinity,
|
|
23
|
+
wrapUpAt: Number.isFinite(limits.wrapUpAt) ? limits.wrapUpAt : Infinity,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** How many calls of the next batch may run, before the loop counts them. */
|
|
28
|
+
export function callsAllowed(state, now = Date.now()) {
|
|
29
|
+
const limits = turnLimitsOf(state.opts);
|
|
30
|
+
if (!limits) return Infinity;
|
|
31
|
+
if (state.turnLimitNoticed || now >= limits.wrapUpAt) return 0;
|
|
32
|
+
return Math.max(0, limits.maxToolCalls - state.toolCallsTotal);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The answer to a call the cap kept from running. */
|
|
36
|
+
export function turnLimitRefusal(call) {
|
|
37
|
+
return { role: 'tool', content: REFUSAL, toolCallId: call.id, toolKind: 'error' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* After a tool batch: `overrun` when the batch came after the turn was told
|
|
42
|
+
* to reply, which ends the turn (state.turnLimitEnded). Otherwise, once a cap
|
|
43
|
+
* is reached, the model is told once to reply.
|
|
44
|
+
*/
|
|
45
|
+
export function settleTurnLimits(state, overrun, now = Date.now()) {
|
|
46
|
+
if (overrun) {
|
|
47
|
+
state.turnLimitEnded = true;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const limits = turnLimitsOf(state.opts);
|
|
51
|
+
if (!limits || state.turnLimitNoticed) return;
|
|
52
|
+
if (state.toolCallsTotal < limits.maxToolCalls && now < limits.wrapUpAt) return;
|
|
53
|
+
state.turnLimitNoticed = true;
|
|
54
|
+
state.messages.push({ role: 'user', content: NOTICE, meta: { source: 'turn-limit' } });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The turn's end after an overrun: no reply. */
|
|
58
|
+
export function endedWithoutReply(response) {
|
|
59
|
+
return { ...response, content: '', toolCalls: [] };
|
|
60
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// its empty-final forensic record), the usage/cost ledger row, and the
|
|
4
4
|
// opaque provider continuation state.
|
|
5
5
|
import { cloneProviderReplay } from '../../providers/lib/provider-replay.mjs';
|
|
6
|
+
import { turnReminderField } from '../loop/assistant-commit.mjs';
|
|
6
7
|
import { logLlmCall } from '../../usage-log.mjs';
|
|
7
8
|
import { runAbortable } from '../../../../shared/abort-race.mjs';
|
|
8
9
|
import { getAgentRuntimeSync } from './agent-runtime-singleton.mjs';
|
|
@@ -31,6 +32,7 @@ export function appendAssistantTurnMessage({ session, sessionId, result, transcr
|
|
|
31
32
|
...(result.providerMetadata && typeof result.providerMetadata === 'object'
|
|
32
33
|
? { providerMetadata: result.providerMetadata }
|
|
33
34
|
: {}),
|
|
35
|
+
...turnReminderField(result),
|
|
34
36
|
// Keep terminal provider evidence for non-empty turns too. A safety
|
|
35
37
|
// classifier can emit narration and then refuse; omitting this
|
|
36
38
|
// metadata made that shape indistinguishable from an ordinary
|
|
@@ -9,8 +9,21 @@ import { updateSessionStage, markSessionStreamDelta } from './runtime-liveness.m
|
|
|
9
9
|
import { codexWireSendOpts } from './session-id.mjs';
|
|
10
10
|
import { persistIterationMetrics } from './usage-metrics.mjs';
|
|
11
11
|
import { isAgentModeSession } from '../agent-mode.mjs';
|
|
12
|
+
import { HEARTBEAT_TURN_LIMITS, isHeartbeatSession } from '../../../../shared/routine-trigger.mjs';
|
|
12
13
|
|
|
13
14
|
const optionalFn = (value) => (typeof value === 'function' ? value : undefined);
|
|
15
|
+
|
|
16
|
+
/** Mixy's heartbeat turn is capped (loop/turn-limits.mjs): its tool calls, and
|
|
17
|
+
* the time after which it must reply. Keyed by the in-process heartbeat mark. */
|
|
18
|
+
export function heartbeatTurnLimits(sessionId, now = Date.now()) {
|
|
19
|
+
if (!isHeartbeatSession(sessionId)) return {};
|
|
20
|
+
return {
|
|
21
|
+
turnLimits: {
|
|
22
|
+
maxToolCalls: HEARTBEAT_TURN_LIMITS.maxToolCalls,
|
|
23
|
+
wrapUpAt: now + HEARTBEAT_TURN_LIMITS.wrapUpMs,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
14
27
|
const relay = (fn, ...args) => {
|
|
15
28
|
try {
|
|
16
29
|
fn?.(...args);
|
|
@@ -201,6 +214,7 @@ export function buildAgentLoopOptions({
|
|
|
201
214
|
relay(askOpts?.onSkillsApplied, names, detail);
|
|
202
215
|
},
|
|
203
216
|
notifyFn: optionalFn(askOpts?.notifyFn),
|
|
217
|
+
...heartbeatTurnLimits(sessionId),
|
|
204
218
|
...buildRequestSendOptions({
|
|
205
219
|
sessionId,
|
|
206
220
|
session,
|
|
@@ -224,7 +224,8 @@ export function publishAskTurn({ sessionId, opened, terminalResultPreview, askOp
|
|
|
224
224
|
// Tag empty-synthesis BEFORE markSessionDone so the watchdog
|
|
225
225
|
// (which inspects entry.emptyFinal first) classifies the
|
|
226
226
|
// terminal state correctly even if it ticks during unwind.
|
|
227
|
-
const isEmptyFinal =
|
|
227
|
+
const isEmptyFinal =
|
|
228
|
+
!terminalResultPreview.content && !terminalResultPreview.reasoningContent && !terminalResultPreview.reactionCompleted;
|
|
228
229
|
if (isEmptyFinal) {
|
|
229
230
|
markSessionEmptyFinal(sessionId);
|
|
230
231
|
}
|
|
@@ -10,6 +10,9 @@ import { isAgentOwner } from '../../agent-owner.mjs';
|
|
|
10
10
|
import { getHiddenAgent } from '../../internal-agents.mjs';
|
|
11
11
|
import { workflowDisallowsAgentTool } from '../../runtime-core/workflow.mjs';
|
|
12
12
|
import { isAgentModeSession } from '../agent-mode.mjs';
|
|
13
|
+
import { agentIdOfHomeSession } from '../agent-home.mjs';
|
|
14
|
+
import { roomMemberOfSession } from '../agent-room.mjs';
|
|
15
|
+
import { REACTION_TOOL_NAME } from '../../runtime-core/reaction-tool-defs.mjs';
|
|
13
16
|
import { finalizeSessionToolList, resolveSessionTools, permissionFromToolSpec } from './tool-resolution.mjs';
|
|
14
17
|
import { unusedModelEditToolName } from '../../../../shared/edit-tool-dialect.mjs';
|
|
15
18
|
import { getAgentRuntimeSync } from './agent-runtime-singleton.mjs';
|
|
@@ -24,6 +27,14 @@ export function delegationDisabled(source, ownerIsAgent) {
|
|
|
24
27
|
);
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
// The react tool belongs to a bot's own chat with the user: its home chat
|
|
31
|
+
// (`sess_home_<id>`) or its seat in a group room (`sess_rmem_…`), decided by the
|
|
32
|
+
// session's id, never a coding session or a delegated worker.
|
|
33
|
+
export function reactionDisabled(source, ownerIsAgent) {
|
|
34
|
+
const id = String(source?.id || '');
|
|
35
|
+
return ownerIsAgent || !(agentIdOfHomeSession(id) || roomMemberOfSession(id));
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
// Exactly two schema surfaces exist: Lead and Agent. Every Agent role gets
|
|
28
39
|
// the full Lead-capable base bundle; the recursive `agent` control tool is
|
|
29
40
|
// removed later at the owner boundary. Role permission remains prompt/
|
|
@@ -79,6 +90,7 @@ export function resolveSessionToolSurface(opts, { profile, toolPreset, modelName
|
|
|
79
90
|
const sessionDeny = [
|
|
80
91
|
...(Array.isArray(opts.disallowedTools) ? opts.disallowedTools : []),
|
|
81
92
|
...(delegationDisabled(opts, ownerIsAgent) ? ['agent'] : []),
|
|
93
|
+
...(reactionDisabled(opts, ownerIsAgent) ? [REACTION_TOOL_NAME] : []),
|
|
82
94
|
];
|
|
83
95
|
// Role permission is prompt/diagnostic metadata only. Resolve and persist
|
|
84
96
|
// it without shaping the provider-visible Agent schema.
|
|
@@ -144,6 +156,7 @@ export function _prepareResumeTools(session, preset) {
|
|
|
144
156
|
disallowedTools: [
|
|
145
157
|
...(Array.isArray(session.disallowedTools) ? session.disallowedTools : []),
|
|
146
158
|
...(delegationDisabled(session, ownerIsAgent) ? ['agent'] : []),
|
|
159
|
+
...(reactionDisabled(session, ownerIsAgent) ? [REACTION_TOOL_NAME] : []),
|
|
147
160
|
],
|
|
148
161
|
ownerIsAgent,
|
|
149
162
|
}),
|
|
@@ -26,7 +26,7 @@ export function messageIdOf(message) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** Whether `meta` can hold a transcript id (absent or a plain object). */
|
|
29
|
-
|
|
29
|
+
function canCarryMessageId(message) {
|
|
30
30
|
if (!message || typeof message !== 'object') return false;
|
|
31
31
|
const meta = message.meta;
|
|
32
32
|
return meta === undefined || meta === null || (typeof meta === 'object' && !Array.isArray(meta));
|
|
@@ -101,12 +101,14 @@ export function stampMessageIds(messages, { sessionId = '' } = {}) {
|
|
|
101
101
|
|
|
102
102
|
/** JSON.stringify replacer that leaves stable ids out: hashes and archives
|
|
103
103
|
* the compactor derives from message JSON describe the conversation, not
|
|
104
|
-
* its display bookkeeping (a meta that only held the id is left out too).
|
|
104
|
+
* its display bookkeeping (a meta that only held the id is left out too).
|
|
105
|
+
* Reactions on a message (message-reactions.mjs) are display bookkeeping of
|
|
106
|
+
* the same kind, written in place mid-turn, and are left out with the id. */
|
|
105
107
|
export function withoutMessageIdsReplacer(key, value) {
|
|
106
108
|
if (key !== 'meta' || !value || typeof value !== 'object' || typeof value.transcript?.messageId !== 'string') {
|
|
107
109
|
return value;
|
|
108
110
|
}
|
|
109
|
-
const { messageId: _id, ...transcript } = value.transcript;
|
|
111
|
+
const { messageId: _id, reactions: _reactions, ...transcript } = value.transcript;
|
|
110
112
|
const meta = { ...value };
|
|
111
113
|
if (Object.keys(transcript).length) meta.transcript = transcript;
|
|
112
114
|
else delete meta.transcript;
|