@awak-app/simy-cli 0.2.0 → 0.2.2
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/README.md +21 -0
- package/package.json +4 -3
- package/src/agent.js +192 -1
- package/src/cli-contract.js +1 -1
- package/src/desktop-executor.js +43 -2
- package/src/direct-executor.js +142 -0
- package/src/execution-guardrail.js +120 -0
- package/src/orchestrator/budget.js +39 -3
- package/src/orchestrator/loop.js +3 -3
- package/src/orchestrator/recovery.js +26 -4
- package/src/runner.js +30 -1
package/README.md
CHANGED
|
@@ -36,6 +36,14 @@ runs. It builds the requirement charter, runs Codex or Claude Code, audits
|
|
|
36
36
|
structured completion evidence, and re-instructs the executor within the
|
|
37
37
|
configured attempt budget.
|
|
38
38
|
|
|
39
|
+
Before Web creates an Agentic Loop, it classifies whether the request actually
|
|
40
|
+
needs a managed PR, verification, evidence, and merge lifecycle. A bounded
|
|
41
|
+
ordinary task is sent to CLI 0.2.2 or newer through the one-time direct executor
|
|
42
|
+
instead. CLI independently recomputes the guardrail, restricts read-only work at
|
|
43
|
+
the Provider command boundary, and permits exactly one Provider invocation. It
|
|
44
|
+
does not create an Agentic Loop ledger, audit session, or retry lifecycle for
|
|
45
|
+
that task.
|
|
46
|
+
|
|
39
47
|
## CLI updates
|
|
40
48
|
|
|
41
49
|
The CLI checks the npm registry at startup and once per hour while it is
|
|
@@ -93,6 +101,19 @@ executor updates, repository selection, and CLI reconnection. Web clients must
|
|
|
93
101
|
validate the action type before rendering or executing it; raw local error text
|
|
94
102
|
is not copied into the recovery contract.
|
|
95
103
|
|
|
104
|
+
## Provider token budget
|
|
105
|
+
|
|
106
|
+
Each run accepts a configurable `token_budget` (or the explicit
|
|
107
|
+
`provider_token_budget` alias) from 1 to 10,000,000 Provider tokens. The default
|
|
108
|
+
is 250,000. Provider tokens are the Codex or Claude Code usage reported for the
|
|
109
|
+
run; they are a safety limit and are not billed as SIMY tokens.
|
|
110
|
+
|
|
111
|
+
When a run reaches this limit, its recovery contract suggests a higher rounded
|
|
112
|
+
limit. SIMY Web can edit that suggestion and call
|
|
113
|
+
`POST /v1/agentic-loop/:run_id/provider-token-budget` with
|
|
114
|
+
`provider_token_budget`. The CLI persists the higher limit and resumes the same
|
|
115
|
+
run. SIMY token consumption remains a separate platform billing record.
|
|
116
|
+
|
|
96
117
|
## Interactive console
|
|
97
118
|
|
|
98
119
|
Running `simy` in a terminal opens the Agentic Loop chat. It discovers the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awak-app/simy-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Local SIMY Agentic Loop executor for Codex and Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
"test:e2e:hypothesis-retries": "node ./scripts/hypothesis-retries-e2e.js",
|
|
26
26
|
"test:e2e:budgets": "node ./scripts/budget-enforcement-e2e.js",
|
|
27
27
|
"test:e2e:desktop-executor": "node ./scripts/desktop-executor-e2e.js",
|
|
28
|
+
"test:e2e:pre-guardrail": "node ./scripts/pre-guardrail-e2e.js",
|
|
28
29
|
"test:e2e:console:real-provider": "node ./scripts/real-provider-console-e2e.js",
|
|
29
30
|
"test:e2e:console:fullstack": "node ./scripts/fullstack-cli-smoke.js",
|
|
30
|
-
"check": "npm run check:auto-update && node --check ./src/desktop-executor.js && node --check ./scripts/desktop-executor-e2e.js && node --check ./scripts/fixtures/fake-desktop-provider.js && npm run check:syntax && npm run check:real-provider-syntax && npm test && npm run check:package",
|
|
31
|
+
"check": "npm run check:auto-update && node --check ./src/desktop-executor.js && node --check ./scripts/desktop-executor-e2e.js && node --check ./scripts/pre-guardrail-e2e.js && node --check ./scripts/fixtures/fake-desktop-provider.js && npm run check:syntax && npm run check:real-provider-syntax && npm test && npm run check:package",
|
|
31
32
|
"check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js",
|
|
32
33
|
"check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js",
|
|
33
|
-
"check:syntax": "node --check ./src/index.js && node --check ./src/agent.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/run-registry.js && node --check ./src/backend-executable.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js",
|
|
34
|
+
"check:syntax": "node --check ./src/index.js && node --check ./src/agent.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js",
|
|
34
35
|
"check:package": "node ./scripts/check-package-contents.js"
|
|
35
36
|
},
|
|
36
37
|
"engines": {
|
package/src/agent.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
continueLocalCodingRun,
|
|
8
8
|
continueLocalCodingRunAfterRepositoryApproval,
|
|
9
9
|
createRun,
|
|
10
|
+
increaseLocalCodingRunProviderTokenBudget,
|
|
10
11
|
LocalRunRegistry,
|
|
11
12
|
isLocalRepositoryApprovalPending,
|
|
12
13
|
pauseLocalCodingRun,
|
|
@@ -69,6 +70,16 @@ import {
|
|
|
69
70
|
CLI_VERSION,
|
|
70
71
|
withCliContract,
|
|
71
72
|
} from "./cli-contract.js";
|
|
73
|
+
import {
|
|
74
|
+
createDirectTask,
|
|
75
|
+
directTaskSnapshot,
|
|
76
|
+
DirectTaskRegistry,
|
|
77
|
+
startDirectTask,
|
|
78
|
+
} from "./direct-executor.js";
|
|
79
|
+
import {
|
|
80
|
+
classifyExecutionRequirement,
|
|
81
|
+
EXECUTION_GUARDRAIL_POLICY_VERSION,
|
|
82
|
+
} from "./execution-guardrail.js";
|
|
72
83
|
|
|
73
84
|
const DEVICE_HEARTBEAT_INTERVAL_MS = 20_000;
|
|
74
85
|
|
|
@@ -83,6 +94,7 @@ export async function startAgent({
|
|
|
83
94
|
} = {}) {
|
|
84
95
|
const apiOrigin = resolveWebOrigin(webOrigin);
|
|
85
96
|
const registry = new LocalRunRegistry();
|
|
97
|
+
const directRegistry = new DirectTaskRegistry();
|
|
86
98
|
const authNonce = randomBytes(16).toString("base64url");
|
|
87
99
|
let session = await readSession(apiOrigin, sessionRoot);
|
|
88
100
|
if (session && sessionRequiresWebAuthorization(session, apiOrigin)) session = null;
|
|
@@ -376,6 +388,112 @@ export async function startAgent({
|
|
|
376
388
|
void synchronizeAuthorizedSession();
|
|
377
389
|
return;
|
|
378
390
|
}
|
|
391
|
+
const directExecutionMatch = url.pathname.match(/^\/v1\/direct-execution\/([^/]+)$/);
|
|
392
|
+
if (req.method === "GET" && directExecutionMatch) {
|
|
393
|
+
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
394
|
+
json(res, 401, { error: "simy session expired; run simy again" });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const task = directRegistry.get(decodeURIComponent(directExecutionMatch[1]));
|
|
398
|
+
if (!task) {
|
|
399
|
+
json(res, 404, { error: "direct task not found" });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
json(res, 200, { task: directTaskSnapshot(task) });
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (req.method === "POST" && url.pathname === "/v1/direct-execution/start") {
|
|
406
|
+
if (!acceptsNewWork(updateManager)) {
|
|
407
|
+
json(res, 503, updateInProgressResponse(updateManager));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
411
|
+
json(res, 401, { error: "simy session expired; run simy again" });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const body = await readJson(req);
|
|
415
|
+
const instruction = String(body.instruction || "").trim();
|
|
416
|
+
if (!instruction) {
|
|
417
|
+
json(res, 400, { error: "instruction is required" });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (body.backend !== "codex" && body.backend !== "claude") {
|
|
421
|
+
json(res, 400, { error: "backend must be codex or claude" });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const guardrail = classifyExecutionRequirement(instruction);
|
|
425
|
+
if (
|
|
426
|
+
guardrail.decision !== "direct_executor" ||
|
|
427
|
+
body.guardrail?.policy_version !== EXECUTION_GUARDRAIL_POLICY_VERSION ||
|
|
428
|
+
body.guardrail?.decision !== "direct_executor"
|
|
429
|
+
) {
|
|
430
|
+
json(res, 409, {
|
|
431
|
+
error: "This request is not authorized for one-time direct execution.",
|
|
432
|
+
code: "DIRECT_EXECUTION_GUARD_REQUIRED",
|
|
433
|
+
guardrail,
|
|
434
|
+
});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (
|
|
438
|
+
body.operation !== guardrail.operation ||
|
|
439
|
+
body.permission_mode !== guardrail.permission_mode ||
|
|
440
|
+
body.max_provider_invocations !== 1
|
|
441
|
+
) {
|
|
442
|
+
json(res, 409, {
|
|
443
|
+
error: "Direct execution permissions do not match the pre-guardrail decision.",
|
|
444
|
+
code: "DIRECT_EXECUTION_GUARD_MISMATCH",
|
|
445
|
+
guardrail,
|
|
446
|
+
});
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const inspection = normalizeBackendInspection(body.backend, availableCapabilities);
|
|
450
|
+
if (!inspection.compatible) {
|
|
451
|
+
const summary = backendPreflightSummary(body.backend, inspection);
|
|
452
|
+
json(res, 409, {
|
|
453
|
+
error: summary,
|
|
454
|
+
code: `desktop_executor_${inspection.status}`,
|
|
455
|
+
});
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const repository = String(body.repository || "").trim() || null;
|
|
459
|
+
if (guardrail.repository_required && !repository) {
|
|
460
|
+
json(res, 400, { error: "repository is required for this direct task" });
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const indexedRepository = repository ? await ensureRepositoryIndexed(repository) : null;
|
|
464
|
+
if (repository && !indexedRepository?.local_path) {
|
|
465
|
+
json(res, 409, {
|
|
466
|
+
error: `${repository} is not authorized on this SIMY CLI.`,
|
|
467
|
+
code: "repository_not_authorized",
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const repositoryPath = indexedRepository?.local_path || workspace.localPath;
|
|
472
|
+
if (!repositoryPath) {
|
|
473
|
+
json(res, 409, {
|
|
474
|
+
error: "No local workspace is available for this direct task.",
|
|
475
|
+
code: "local_workspace_unavailable",
|
|
476
|
+
});
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const task = createDirectTask({
|
|
480
|
+
instruction,
|
|
481
|
+
backend: body.backend,
|
|
482
|
+
operation: guardrail.operation,
|
|
483
|
+
permissionMode: guardrail.permission_mode,
|
|
484
|
+
repository,
|
|
485
|
+
repositoryPath,
|
|
486
|
+
});
|
|
487
|
+
directRegistry.create(task);
|
|
488
|
+
const runDirectTask = dependencies.runDirectTask || startDirectTask;
|
|
489
|
+
void Promise.resolve(runDirectTask(task)).catch((error) => {
|
|
490
|
+
task.status = "failed";
|
|
491
|
+
task.error = error instanceof Error ? error.message : String(error);
|
|
492
|
+
task.updated_at = new Date().toISOString();
|
|
493
|
+
});
|
|
494
|
+
json(res, 202, { ok: true, task: directTaskSnapshot(task) });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
379
497
|
if (req.method === "POST" && agenticLoopPath === "/v1/agentic-loop/start") {
|
|
380
498
|
if (!acceptsNewWork(updateManager)) {
|
|
381
499
|
json(res, 503, updateInProgressResponse(updateManager));
|
|
@@ -386,6 +504,22 @@ export async function startAgent({
|
|
|
386
504
|
return;
|
|
387
505
|
}
|
|
388
506
|
const { body, attachments } = await readCodingLoopStart(req);
|
|
507
|
+
const guardrailSource = String(body.source_message || "").trim();
|
|
508
|
+
if (guardrailSource) {
|
|
509
|
+
const guardrail = classifyExecutionRequirement(guardrailSource);
|
|
510
|
+
if (
|
|
511
|
+
guardrail.decision !== "agentic_loop" ||
|
|
512
|
+
body.guardrail?.policy_version !== EXECUTION_GUARDRAIL_POLICY_VERSION ||
|
|
513
|
+
body.guardrail?.decision !== "agentic_loop"
|
|
514
|
+
) {
|
|
515
|
+
json(res, 409, {
|
|
516
|
+
error: "This task does not qualify for an Agentic Loop.",
|
|
517
|
+
code: "AGENTIC_LOOP_NOT_REQUIRED",
|
|
518
|
+
guardrail,
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
389
523
|
const verified = await verifyLaunchChallenge({
|
|
390
524
|
apiOrigin,
|
|
391
525
|
apiBaseUrl: session.api_base_url,
|
|
@@ -468,7 +602,7 @@ export async function startAgent({
|
|
|
468
602
|
base_branch: typeof body.base_branch === "string" ? body.base_branch : "dev",
|
|
469
603
|
max_attempts: body.max_attempts,
|
|
470
604
|
retry_budget: body.retry_budget,
|
|
471
|
-
token_budget: body.token_budget,
|
|
605
|
+
token_budget: body.provider_token_budget ?? body.token_budget,
|
|
472
606
|
ui_evidence_root:
|
|
473
607
|
typeof body.ui_evidence_root === "string" ? body.ui_evidence_root : "",
|
|
474
608
|
acceptance_criteria: Array.isArray(body.acceptance_criteria)
|
|
@@ -553,6 +687,53 @@ export async function startAgent({
|
|
|
553
687
|
return;
|
|
554
688
|
}
|
|
555
689
|
|
|
690
|
+
const providerBudgetMatch = agenticLoopPath.match(
|
|
691
|
+
/^\/v1\/agentic-loop\/([^/]+)\/provider-token-budget$/,
|
|
692
|
+
);
|
|
693
|
+
if (req.method === "POST" && providerBudgetMatch) {
|
|
694
|
+
if (!acceptsNewWork(updateManager)) {
|
|
695
|
+
json(res, 503, updateInProgressResponse(updateManager));
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
699
|
+
json(res, 401, { error: "simy session expired; run simy again" });
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const run = registry.get(decodeURIComponent(providerBudgetMatch[1]));
|
|
703
|
+
if (!run) {
|
|
704
|
+
json(res, 404, { error: "run not found" });
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const body = await readJson(req);
|
|
708
|
+
let budget;
|
|
709
|
+
try {
|
|
710
|
+
budget = await increaseLocalCodingRunProviderTokenBudget(
|
|
711
|
+
run,
|
|
712
|
+
body.provider_token_budget,
|
|
713
|
+
);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
json(res, 409, {
|
|
716
|
+
error: error instanceof Error ? error.message : "Provider token budget was rejected",
|
|
717
|
+
code: "provider_token_budget_rejected",
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const continuation = continueLocalCodingRun(
|
|
722
|
+
run,
|
|
723
|
+
String(body.message || "Continue after increasing the Provider token budget."),
|
|
724
|
+
runOptions,
|
|
725
|
+
);
|
|
726
|
+
json(res, 202, {
|
|
727
|
+
ok: true,
|
|
728
|
+
run_id: run.id,
|
|
729
|
+
state: "resuming",
|
|
730
|
+
provider_token_budget: budget.token_budget,
|
|
731
|
+
provider_tokens_used: budget.tokens_used,
|
|
732
|
+
});
|
|
733
|
+
void continuation.catch((error) => reportProviderBudgetResumeError(error, quiet));
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
556
737
|
const guidanceMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/guidance$/);
|
|
557
738
|
if (req.method === "POST" && guidanceMatch) {
|
|
558
739
|
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
@@ -743,6 +924,7 @@ export async function startAgent({
|
|
|
743
924
|
server,
|
|
744
925
|
port,
|
|
745
926
|
registry,
|
|
927
|
+
directRegistry,
|
|
746
928
|
webOrigin: apiOrigin,
|
|
747
929
|
get loginUrl() {
|
|
748
930
|
return loginUrl?.toString() ?? null;
|
|
@@ -964,6 +1146,14 @@ function reportRepositoryResumeError(error, quiet) {
|
|
|
964
1146
|
);
|
|
965
1147
|
}
|
|
966
1148
|
|
|
1149
|
+
function reportProviderBudgetResumeError(error, quiet) {
|
|
1150
|
+
if (!quiet) {
|
|
1151
|
+
console.error(
|
|
1152
|
+
`Provider token budget was increased, but the run could not resume: ${error instanceof Error ? error.message : error}`,
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
967
1157
|
function localRepositoryHilRequestId(run) {
|
|
968
1158
|
return `local-repository-scan:${run.id}`;
|
|
969
1159
|
}
|
|
@@ -1021,6 +1211,7 @@ async function capabilities() {
|
|
|
1021
1211
|
repository_scan_approval: true,
|
|
1022
1212
|
executor_version_preflight: true,
|
|
1023
1213
|
desktop_executor: true,
|
|
1214
|
+
direct_execution: true,
|
|
1024
1215
|
},
|
|
1025
1216
|
session_ttl_hours: 48,
|
|
1026
1217
|
};
|
package/src/cli-contract.js
CHANGED
package/src/desktop-executor.js
CHANGED
|
@@ -49,11 +49,18 @@ export async function resolveDesktopExecutorCommand({
|
|
|
49
49
|
backend,
|
|
50
50
|
instruction,
|
|
51
51
|
repositoryPath,
|
|
52
|
+
executionKind = "agentic_loop",
|
|
53
|
+
permissionMode = "workspace_write",
|
|
52
54
|
environment = process.env,
|
|
53
55
|
spawnImpl = spawn,
|
|
54
56
|
inspectionOptions = {},
|
|
55
57
|
} = {}) {
|
|
56
58
|
const provider = normalizeBackend(backend);
|
|
59
|
+
const directExecution = executionKind === "direct";
|
|
60
|
+
if (executionKind !== "agentic_loop" && !directExecution) {
|
|
61
|
+
throw new Error("Desktop execution kind must be agentic_loop or direct.");
|
|
62
|
+
}
|
|
63
|
+
const normalizedPermissionMode = normalizePermissionMode(permissionMode);
|
|
57
64
|
const target = normalizeDesktopExecutionTarget(DESKTOP_EXECUTION_TARGET);
|
|
58
65
|
const override =
|
|
59
66
|
provider === "claude" ? environment.SIMY_CLAUDE_COMMAND : environment.SIMY_CODEX_COMMAND;
|
|
@@ -62,6 +69,8 @@ export async function resolveDesktopExecutorCommand({
|
|
|
62
69
|
...shellCommand(override, repositoryPath, instruction, spawnImpl),
|
|
63
70
|
backend: provider,
|
|
64
71
|
execution_target: target,
|
|
72
|
+
execution_kind: executionKind,
|
|
73
|
+
permission_mode: normalizedPermissionMode,
|
|
65
74
|
verification: "explicit_command_override",
|
|
66
75
|
};
|
|
67
76
|
}
|
|
@@ -77,12 +86,24 @@ export async function resolveDesktopExecutorCommand({
|
|
|
77
86
|
bin: inspection.executable,
|
|
78
87
|
args:
|
|
79
88
|
provider === "claude"
|
|
80
|
-
?
|
|
81
|
-
|
|
89
|
+
? directExecution
|
|
90
|
+
? claudeDirectBackendArgs(instruction, normalizedPermissionMode)
|
|
91
|
+
: claudeBackendArgs(instruction)
|
|
92
|
+
: directExecution
|
|
93
|
+
? [
|
|
94
|
+
"exec",
|
|
95
|
+
"--json",
|
|
96
|
+
"--sandbox",
|
|
97
|
+
normalizedPermissionMode === "read_only" ? "read-only" : "workspace-write",
|
|
98
|
+
instruction,
|
|
99
|
+
]
|
|
100
|
+
: ["exec", "--json", instruction],
|
|
82
101
|
env: {},
|
|
83
102
|
spawn: spawnImpl,
|
|
84
103
|
backend: provider,
|
|
85
104
|
execution_target: target,
|
|
105
|
+
execution_kind: executionKind,
|
|
106
|
+
permission_mode: normalizedPermissionMode,
|
|
86
107
|
verification: "compatible_version",
|
|
87
108
|
installed_version: inspection.installed_version,
|
|
88
109
|
};
|
|
@@ -102,11 +123,30 @@ export function claudeBackendArgs(instruction) {
|
|
|
102
123
|
];
|
|
103
124
|
}
|
|
104
125
|
|
|
126
|
+
export function claudeDirectBackendArgs(instruction, permissionMode = "read_only") {
|
|
127
|
+
const normalized = normalizePermissionMode(permissionMode);
|
|
128
|
+
return [
|
|
129
|
+
"-p",
|
|
130
|
+
instruction,
|
|
131
|
+
"--output-format",
|
|
132
|
+
"stream-json",
|
|
133
|
+
"--verbose",
|
|
134
|
+
...(normalized === "read_only"
|
|
135
|
+
? ["--permission-mode", "plan"]
|
|
136
|
+
: ["--dangerously-skip-permissions"]),
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
|
|
105
140
|
function normalizeBackend(backend) {
|
|
106
141
|
if (backend === "codex" || backend === "claude") return backend;
|
|
107
142
|
throw new Error("Desktop executor backend must be codex or claude.");
|
|
108
143
|
}
|
|
109
144
|
|
|
145
|
+
function normalizePermissionMode(value) {
|
|
146
|
+
if (value === "read_only" || value === "workspace_write") return value;
|
|
147
|
+
throw new Error("Desktop permission mode must be read_only or workspace_write.");
|
|
148
|
+
}
|
|
149
|
+
|
|
110
150
|
function desktopExecutorUnavailableError(inspection) {
|
|
111
151
|
const provider = inspection.provider_label;
|
|
112
152
|
let message;
|
|
@@ -131,6 +171,7 @@ function shellCommand(command, cwd, instruction, spawnImpl) {
|
|
|
131
171
|
env: {
|
|
132
172
|
SIMY_AGENTIC_LOOP_REQUIREMENT: instruction,
|
|
133
173
|
SIMY_CODING_LOOP_REQUIREMENT: instruction,
|
|
174
|
+
SIMY_DIRECT_EXECUTION_INSTRUCTION: instruction,
|
|
134
175
|
},
|
|
135
176
|
spawn: spawnImpl,
|
|
136
177
|
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { resolveDesktopExecutorCommand } from "./desktop-executor.js";
|
|
4
|
+
import { createProviderStreamDecoder } from "./provider-stream.js";
|
|
5
|
+
|
|
6
|
+
const MAX_OUTPUT_LINES = 400;
|
|
7
|
+
|
|
8
|
+
export class DirectTaskRegistry {
|
|
9
|
+
#tasks = new Map();
|
|
10
|
+
|
|
11
|
+
create(task) {
|
|
12
|
+
if (this.#tasks.has(task.id)) throw new Error(`Direct task ${task.id} already exists.`);
|
|
13
|
+
this.#tasks.set(task.id, task);
|
|
14
|
+
return task;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get(taskId) {
|
|
18
|
+
return this.#tasks.get(taskId) ?? null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createDirectTask({
|
|
23
|
+
instruction,
|
|
24
|
+
backend,
|
|
25
|
+
operation,
|
|
26
|
+
permissionMode,
|
|
27
|
+
repository = null,
|
|
28
|
+
repositoryPath,
|
|
29
|
+
}) {
|
|
30
|
+
const now = new Date().toISOString();
|
|
31
|
+
return {
|
|
32
|
+
id: `direct_task_${randomUUID()}`,
|
|
33
|
+
status: "queued",
|
|
34
|
+
instruction,
|
|
35
|
+
backend,
|
|
36
|
+
operation,
|
|
37
|
+
permission_mode: permissionMode,
|
|
38
|
+
repository,
|
|
39
|
+
repository_path: repositoryPath,
|
|
40
|
+
invocation_count: 0,
|
|
41
|
+
output: [],
|
|
42
|
+
result: null,
|
|
43
|
+
error: null,
|
|
44
|
+
token_usage: { records: [] },
|
|
45
|
+
created_at: now,
|
|
46
|
+
updated_at: now,
|
|
47
|
+
child: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function startDirectTask(
|
|
52
|
+
task,
|
|
53
|
+
{ resolveCommand = resolveDesktopExecutorCommand, environment = process.env } = {},
|
|
54
|
+
) {
|
|
55
|
+
if (!task || task.status !== "queued") throw new Error("Direct task must be queued.");
|
|
56
|
+
if (task.invocation_count >= 1) throw new Error("Direct tasks allow only one Provider invocation.");
|
|
57
|
+
const command = await resolveCommand({
|
|
58
|
+
backend: task.backend,
|
|
59
|
+
instruction: task.instruction,
|
|
60
|
+
repositoryPath: task.repository_path,
|
|
61
|
+
executionKind: "direct",
|
|
62
|
+
permissionMode: task.permission_mode,
|
|
63
|
+
environment,
|
|
64
|
+
});
|
|
65
|
+
task.status = "running";
|
|
66
|
+
task.invocation_count = 1;
|
|
67
|
+
task.updated_at = new Date().toISOString();
|
|
68
|
+
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
let settled = false;
|
|
71
|
+
const rawStdout = [];
|
|
72
|
+
const tokenRecords = [];
|
|
73
|
+
const child = command.spawn(command.bin, command.args, {
|
|
74
|
+
cwd: task.repository_path,
|
|
75
|
+
env: { ...process.env, ...command.env },
|
|
76
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
77
|
+
detached: process.platform !== "win32",
|
|
78
|
+
});
|
|
79
|
+
task.child = child;
|
|
80
|
+
const onLine = (line) => {
|
|
81
|
+
const normalized = String(line || "").trim();
|
|
82
|
+
if (!normalized) return;
|
|
83
|
+
task.output.push(normalized);
|
|
84
|
+
if (task.output.length > MAX_OUTPUT_LINES) {
|
|
85
|
+
task.output.splice(0, task.output.length - MAX_OUTPUT_LINES);
|
|
86
|
+
}
|
|
87
|
+
task.updated_at = new Date().toISOString();
|
|
88
|
+
};
|
|
89
|
+
const stdoutDecoder = createProviderStreamDecoder({
|
|
90
|
+
backend: task.backend,
|
|
91
|
+
stream: "stdout",
|
|
92
|
+
onLine,
|
|
93
|
+
onUsage: (usage) => tokenRecords.push({ ...usage, phase: "direct", backend: task.backend }),
|
|
94
|
+
});
|
|
95
|
+
const stderrDecoder = createProviderStreamDecoder({
|
|
96
|
+
backend: task.backend,
|
|
97
|
+
stream: "stderr",
|
|
98
|
+
onLine,
|
|
99
|
+
});
|
|
100
|
+
child.stdout?.on("data", (chunk) => {
|
|
101
|
+
const text = chunk.toString("utf8");
|
|
102
|
+
rawStdout.push(text);
|
|
103
|
+
stdoutDecoder.push(text);
|
|
104
|
+
});
|
|
105
|
+
child.stderr?.on("data", (chunk) => stderrDecoder.push(chunk.toString("utf8")));
|
|
106
|
+
|
|
107
|
+
const finish = ({ exitCode = null, error = null } = {}) => {
|
|
108
|
+
if (settled) return;
|
|
109
|
+
settled = true;
|
|
110
|
+
stdoutDecoder.flush();
|
|
111
|
+
stderrDecoder.flush();
|
|
112
|
+
task.child = null;
|
|
113
|
+
task.token_usage = { records: tokenRecords };
|
|
114
|
+
task.status = error || exitCode !== 0 ? "failed" : "succeeded";
|
|
115
|
+
task.error = error || (exitCode !== 0 ? `Provider exited with code ${exitCode}.` : null);
|
|
116
|
+
task.result = task.output.at(-1) || rawStdout.join("").trim() || null;
|
|
117
|
+
task.updated_at = new Date().toISOString();
|
|
118
|
+
resolve(task);
|
|
119
|
+
};
|
|
120
|
+
child.on("error", (error) => finish({ error: error.message }));
|
|
121
|
+
child.on("close", (code) => finish({ exitCode: code }));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function directTaskSnapshot(task) {
|
|
126
|
+
return {
|
|
127
|
+
id: task.id,
|
|
128
|
+
status: task.status,
|
|
129
|
+
instruction: task.instruction,
|
|
130
|
+
backend: task.backend,
|
|
131
|
+
operation: task.operation,
|
|
132
|
+
permission_mode: task.permission_mode,
|
|
133
|
+
repository: task.repository,
|
|
134
|
+
invocation_count: task.invocation_count,
|
|
135
|
+
output: [...task.output],
|
|
136
|
+
result: task.result,
|
|
137
|
+
error: task.error,
|
|
138
|
+
token_usage: structuredClone(task.token_usage),
|
|
139
|
+
created_at: task.created_at,
|
|
140
|
+
updated_at: task.updated_at,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export const EXECUTION_GUARDRAIL_POLICY_VERSION = "2026-07-21.1";
|
|
2
|
+
|
|
3
|
+
const OBSERVE_PATTERN =
|
|
4
|
+
/\b(monitor|watch|observe|track|status|progress|keep an eye on)\b|監視|見守|進捗|状況を?(?:見|確認)|监控|監控|观察|觀察|进度|進度|状态|狀態/i;
|
|
5
|
+
const READ_PATTERN =
|
|
6
|
+
/\b(explain|summari[sz]e|inspect|review|look at|read|check|diagnose|investigate)\b|説明|要約|確認|調査|レビュー|查看|解释|說明|总结|總結|检查|檢查|调查|調查|诊断|診斷/i;
|
|
7
|
+
const CHANGE_PATTERN =
|
|
8
|
+
/\b(implement|fix|build|create|change|update|refactor|add|remove|rename|ship|write|edit|merge|release|publish)\b|実装|修正|改善|変更|追加|削除|作成|開発|マージ|公開|发布|發佈|实现|實現|修复|修復|修改|改掉|改一下|新增|删除|刪除|重构|重構|合并|合併/i;
|
|
9
|
+
const OBJECT_PATTERN =
|
|
10
|
+
/\b(code|file|readme|api|ui|ux|feature|bug|pr|pull request|repository|repo|branch|test|migration|endpoint|component|page|cli|backend|extension)\b|コード|ファイル|機能|不具合|画面|ページ|テスト|移行|组件|組件|页面|頁面|代码|代碼|文件|功能|错误|錯誤|仓库|倉庫|分支/i;
|
|
11
|
+
const VAGUE_PATTERN = /^(?:fix|do|handle|continue|change)?\s*(?:it|this|that)?[.!。!]?$/i;
|
|
12
|
+
const VAGUE_CJK_PATTERN = /^(?:これ|それ|あれ|やって|直して|続けて|这个|這個|那个|那個|处理|處理|修一下)[。!]?$/i;
|
|
13
|
+
const UNSUPPORTED_PATTERN =
|
|
14
|
+
/\b(?:steal|exfiltrate|bypass authentication|disable audit)\b|認証を迂回|監査を無効|窃取凭证|繞過認證/i;
|
|
15
|
+
|
|
16
|
+
const MANAGED_SIGNAL_PATTERNS = [
|
|
17
|
+
["pr_merge_release", /\b(?:pull request|pr|merge|release|publish|deploy)\b|PR|マージ|リリース|デプロイ|合并|合併|发布|發佈|部署/i],
|
|
18
|
+
["verification", /\b(?:test|e2e|integration test|until green|ci)\b|テスト|検証|通过测试|通過測試|直到.*通过|直到.*通過/i],
|
|
19
|
+
["browser_evidence", /\b(?:browser|screenshots?|visual evidence|evidence)\b|ブラウザ|スクリーンショット|証跡|截图|截圖|浏览器|瀏覽器|证据|證據/i],
|
|
20
|
+
["cross_repository", /\b(?:cross[- ]repo|multiple repos|multi[- ]repo)\b|複数(?:の)?リポジトリ|跨仓库|跨倉庫|多个仓库|多個倉庫/i],
|
|
21
|
+
["high_risk_change", /\b(?:migration|authentication|authorization|security|rollback)\b|移行|認証|認可|セキュリティ|ロールバック|迁移|遷移|认证|認證|授权|授權|安全|回滚|回滾/i],
|
|
22
|
+
["diagnose_repair_reverify", /\bdiagnos\w*\b[\s\S]*\bfix\w*\b[\s\S]*\b(?:verify|retest)\b|調査[\s\S]*修正[\s\S]*再検証|诊断[\s\S]*修复[\s\S]*复验|診斷[\s\S]*修復[\s\S]*複驗/i],
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export function classifyExecutionRequirement(sourceMessage) {
|
|
26
|
+
const source = String(sourceMessage || "")
|
|
27
|
+
.replace(/^\s*\/(?:agentic-loop|coding-loop)\b\s*/i, "")
|
|
28
|
+
.trim();
|
|
29
|
+
if (!source || VAGUE_PATTERN.test(source) || VAGUE_CJK_PATTERN.test(source)) {
|
|
30
|
+
const vagueChangeRequested = CHANGE_PATTERN.test(source);
|
|
31
|
+
return guardrail("needs_clarification", {
|
|
32
|
+
change_requested: vagueChangeRequested,
|
|
33
|
+
operation: vagueChangeRequested ? "execute" : "read",
|
|
34
|
+
permission_mode: vagueChangeRequested ? "workspace_write" : "read_only",
|
|
35
|
+
reason_code: "goal_not_clear",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (UNSUPPORTED_PATTERN.test(source)) {
|
|
39
|
+
return guardrail("unsupported", {
|
|
40
|
+
operation: "execute",
|
|
41
|
+
permission_mode: "workspace_write",
|
|
42
|
+
reason_code: "prohibited_operation",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const changeRequested = CHANGE_PATTERN.test(source);
|
|
46
|
+
const observeRequested = OBSERVE_PATTERN.test(source);
|
|
47
|
+
const readRequested = READ_PATTERN.test(source);
|
|
48
|
+
const hasObject = OBJECT_PATTERN.test(source) || source.length >= 24;
|
|
49
|
+
const agenticSignals = MANAGED_SIGNAL_PATTERNS.filter(([, pattern]) => pattern.test(source)).map(
|
|
50
|
+
([signal]) => signal,
|
|
51
|
+
);
|
|
52
|
+
if (changeRequested && hasObject && agenticSignals.length >= 2) {
|
|
53
|
+
return guardrail("agentic_loop", {
|
|
54
|
+
change_requested: true,
|
|
55
|
+
managed_lifecycle_required: true,
|
|
56
|
+
repository_required: true,
|
|
57
|
+
operation: "execute",
|
|
58
|
+
permission_mode: "workspace_write",
|
|
59
|
+
agentic_signals: agenticSignals,
|
|
60
|
+
reason_code: "managed_change_lifecycle_required",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (observeRequested && !changeRequested) {
|
|
64
|
+
return guardrail("direct_executor", {
|
|
65
|
+
repository_required: /\b(?:repo|repository|pr|branch)\b|リポジトリ|仓库|倉庫|分支/i.test(source),
|
|
66
|
+
operation: "observe",
|
|
67
|
+
permission_mode: "read_only",
|
|
68
|
+
direct_signals: ["bounded_observation", "no_persistent_change"],
|
|
69
|
+
reason_code: "observe_existing_provider_session",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (changeRequested && hasObject) {
|
|
73
|
+
return guardrail("direct_executor", {
|
|
74
|
+
change_requested: true,
|
|
75
|
+
repository_required: true,
|
|
76
|
+
operation: "execute",
|
|
77
|
+
permission_mode: "workspace_write",
|
|
78
|
+
agentic_signals: agenticSignals,
|
|
79
|
+
direct_signals: ["single_bounded_change", "managed_lifecycle_not_required"],
|
|
80
|
+
reason_code: "bounded_change_direct_execution",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (changeRequested) {
|
|
84
|
+
return guardrail("needs_clarification", {
|
|
85
|
+
change_requested: true,
|
|
86
|
+
operation: "execute",
|
|
87
|
+
permission_mode: "workspace_write",
|
|
88
|
+
reason_code: "change_target_missing",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (readRequested || source.length >= 8) {
|
|
92
|
+
return guardrail("direct_executor", {
|
|
93
|
+
repository_required: /\b(?:repo|repository|pr|file|code)\b|コード|ファイル|仓库|倉庫|文件|代码|代碼/i.test(source),
|
|
94
|
+
operation: "read",
|
|
95
|
+
permission_mode: "read_only",
|
|
96
|
+
direct_signals: ["single_read_task", "no_managed_lifecycle"],
|
|
97
|
+
reason_code: "bounded_read_direct_execution",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return guardrail("needs_clarification", {
|
|
101
|
+
operation: "read",
|
|
102
|
+
permission_mode: "read_only",
|
|
103
|
+
reason_code: "action_missing",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function guardrail(decision, overrides) {
|
|
108
|
+
return {
|
|
109
|
+
policy_version: EXECUTION_GUARDRAIL_POLICY_VERSION,
|
|
110
|
+
decision,
|
|
111
|
+
change_requested: false,
|
|
112
|
+
managed_lifecycle_required: false,
|
|
113
|
+
repository_required: false,
|
|
114
|
+
operation: "read",
|
|
115
|
+
permission_mode: "read_only",
|
|
116
|
+
agentic_signals: [],
|
|
117
|
+
direct_signals: [],
|
|
118
|
+
...overrides,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -2,7 +2,8 @@ import { clampAttempts } from "./shared.js";
|
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_TOKEN_BUDGET = 250_000;
|
|
4
4
|
export const DEFAULT_RETRY_BUDGET = 2;
|
|
5
|
-
const
|
|
5
|
+
export const MAX_PROVIDER_TOKEN_BUDGET = 10_000_000;
|
|
6
|
+
const PROVIDER_TOKEN_BUDGET_STEP = 250_000;
|
|
6
7
|
|
|
7
8
|
export function resolveRunBudgets(request = {}) {
|
|
8
9
|
const legacyMaxAttempts = clampAttempts(request.max_attempts);
|
|
@@ -12,12 +13,47 @@ export function resolveRunBudgets(request = {}) {
|
|
|
12
13
|
? legacyMaxAttempts - 1
|
|
13
14
|
: Math.min(4, Math.max(0, explicitRetryBudget));
|
|
14
15
|
return {
|
|
15
|
-
token_budget: normalizeTokenBudget(request.token_budget),
|
|
16
|
+
token_budget: normalizeTokenBudget(request.provider_token_budget ?? request.token_budget),
|
|
16
17
|
retry_budget: retryBudget,
|
|
17
18
|
max_attempts: retryBudget + 1,
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
export function suggestedProviderTokenBudget(value) {
|
|
23
|
+
const state = value?.schema_version === 1 ? value : budgetState(value);
|
|
24
|
+
if (state.tokens_used >= MAX_PROVIDER_TOKEN_BUDGET) return null;
|
|
25
|
+
const required = Math.max(
|
|
26
|
+
state.token_budget * 2,
|
|
27
|
+
state.tokens_used * 1.25,
|
|
28
|
+
state.tokens_used + PROVIDER_TOKEN_BUDGET_STEP,
|
|
29
|
+
);
|
|
30
|
+
return Math.min(
|
|
31
|
+
MAX_PROVIDER_TOKEN_BUDGET,
|
|
32
|
+
Math.ceil(required / PROVIDER_TOKEN_BUDGET_STEP) * PROVIDER_TOKEN_BUDGET_STEP,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function increaseProviderTokenBudget(snapshot, value) {
|
|
37
|
+
const requested = integer(value);
|
|
38
|
+
if (requested === null || requested <= 0) {
|
|
39
|
+
throw new Error("Provider token budget must be a positive integer.");
|
|
40
|
+
}
|
|
41
|
+
if (requested > MAX_PROVIDER_TOKEN_BUDGET) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Provider token budget cannot exceed ${MAX_PROVIDER_TOKEN_BUDGET.toLocaleString("en-US")}.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const current = budgetState(snapshot);
|
|
47
|
+
if (requested <= current.token_budget) {
|
|
48
|
+
throw new Error("Provider token budget must be higher than the current limit.");
|
|
49
|
+
}
|
|
50
|
+
if (requested <= current.tokens_used) {
|
|
51
|
+
throw new Error("Provider token budget must be higher than the Provider tokens already used.");
|
|
52
|
+
}
|
|
53
|
+
snapshot.charter.token_budget = requested;
|
|
54
|
+
return refreshBudgetState(snapshot);
|
|
55
|
+
}
|
|
56
|
+
|
|
21
57
|
export function budgetState(snapshot, pendingAttempt = null) {
|
|
22
58
|
const attempts = [
|
|
23
59
|
...(Array.isArray(snapshot?.attempts) ? snapshot.attempts : []),
|
|
@@ -93,7 +129,7 @@ function recordTokenTotal(value) {
|
|
|
93
129
|
function normalizeTokenBudget(value) {
|
|
94
130
|
const parsed = integer(value);
|
|
95
131
|
if (parsed === null || parsed <= 0) return DEFAULT_TOKEN_BUDGET;
|
|
96
|
-
return Math.min(
|
|
132
|
+
return Math.min(MAX_PROVIDER_TOKEN_BUDGET, parsed);
|
|
97
133
|
}
|
|
98
134
|
|
|
99
135
|
function normalizeRetryBudget(value, maxAttempts) {
|
package/src/orchestrator/loop.js
CHANGED
|
@@ -469,7 +469,7 @@ async function stopForBudget({ snapshot, attempt, reason, onUpdate }) {
|
|
|
469
469
|
if (attempt) attempt.budget = budget;
|
|
470
470
|
const message =
|
|
471
471
|
reason === "token_budget_exhausted"
|
|
472
|
-
? "Agentic loop
|
|
472
|
+
? "Agentic loop reached its Provider token budget."
|
|
473
473
|
: "Agentic loop exhausted its retry budget.";
|
|
474
474
|
appendEvent(snapshot, "blocked", message, {
|
|
475
475
|
code: reason,
|
|
@@ -487,8 +487,8 @@ function budgetFinding(reason, budget) {
|
|
|
487
487
|
passed: false,
|
|
488
488
|
code: "TOKEN_BUDGET_EXHAUSTED",
|
|
489
489
|
severity: "blocker",
|
|
490
|
-
target: "
|
|
491
|
-
explanation: `The run used ${budget.tokens_used} of ${budget.token_budget}
|
|
490
|
+
target: "provider_token_budget",
|
|
491
|
+
explanation: `The run used ${budget.tokens_used} of ${budget.token_budget} Provider tokens, so no additional executor or reviewer call can start until the Provider token budget is increased. Provider tokens are not billed as SIMY tokens.`,
|
|
492
492
|
repairability: "manual",
|
|
493
493
|
auto_fix_hint: null,
|
|
494
494
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { suggestedProviderTokenBudget } from "./budget.js";
|
|
2
|
+
|
|
1
3
|
const RECOVERABLE_STATES = new Set(["waiting_human", "blocked", "failed"]);
|
|
2
4
|
|
|
3
5
|
export function recoveryContractForEvent({ snapshot, state, message, detail = {} }) {
|
|
@@ -6,7 +8,7 @@ export function recoveryContractForEvent({ snapshot, state, message, detail = {}
|
|
|
6
8
|
|
|
7
9
|
const reasonCode = recoveryReasonCode(state, message, detail);
|
|
8
10
|
const attemptNumber = positiveInteger(detail.attempt_number);
|
|
9
|
-
const actions = recoveryActions(reasonCode);
|
|
11
|
+
const actions = recoveryActions(reasonCode, detail.budget);
|
|
10
12
|
return {
|
|
11
13
|
schema_version: 1,
|
|
12
14
|
state,
|
|
@@ -18,6 +20,8 @@ export function recoveryContractForEvent({ snapshot, state, message, detail = {}
|
|
|
18
20
|
event_code: clean(detail.code) || null,
|
|
19
21
|
attempt_number: attemptNumber,
|
|
20
22
|
max_attempts: positiveInteger(snapshot?.charter?.max_attempts),
|
|
23
|
+
provider_token_budget: positiveInteger(snapshot?.charter?.token_budget),
|
|
24
|
+
provider_tokens_used: nonNegativeInteger(detail?.budget?.tokens_used),
|
|
21
25
|
token_budget: positiveInteger(snapshot?.charter?.token_budget),
|
|
22
26
|
retry_budget: nonNegativeInteger(snapshot?.charter?.retry_budget),
|
|
23
27
|
tokens_used: nonNegativeInteger(detail?.budget?.tokens_used),
|
|
@@ -136,7 +140,7 @@ function recoveryMessage(reasonCode) {
|
|
|
136
140
|
"SIMY needs the missing requirement or approval before implementation can start.",
|
|
137
141
|
human_input_required: "SIMY needs your decision or clarification before it can continue.",
|
|
138
142
|
token_budget_exhausted:
|
|
139
|
-
"This run
|
|
143
|
+
"This run reached its Provider token safety limit. Provider tokens are not billed by SIMY. Increase the limit to continue this run.",
|
|
140
144
|
retry_budget_exhausted:
|
|
141
145
|
"This run used its automatic retry budget. Start a new run to continue with a fresh limit.",
|
|
142
146
|
automatic_progress_blocked:
|
|
@@ -149,7 +153,7 @@ function recoveryMessage(reasonCode) {
|
|
|
149
153
|
return messages[reasonCode] || messages.automatic_progress_blocked;
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
function recoveryActions(reasonCode) {
|
|
156
|
+
function recoveryActions(reasonCode, budget = null) {
|
|
153
157
|
if (reasonCode === "repository_authorization_required") {
|
|
154
158
|
return [
|
|
155
159
|
{
|
|
@@ -182,7 +186,25 @@ function recoveryActions(reasonCode) {
|
|
|
182
186
|
newRunAction(),
|
|
183
187
|
];
|
|
184
188
|
}
|
|
185
|
-
if (reasonCode === "token_budget_exhausted"
|
|
189
|
+
if (reasonCode === "token_budget_exhausted") {
|
|
190
|
+
const suggestedBudget = suggestedProviderTokenBudget(budget);
|
|
191
|
+
if (!suggestedBudget || budget?.retry_exhausted === true) return [newRunAction()];
|
|
192
|
+
return [
|
|
193
|
+
{
|
|
194
|
+
id: "increase_provider_token_budget",
|
|
195
|
+
type: "increase_provider_token_budget",
|
|
196
|
+
label: "Increase Provider token budget",
|
|
197
|
+
description:
|
|
198
|
+
"Raise this run's safety limit and continue. Provider tokens are usage diagnostics and are not billed as SIMY tokens.",
|
|
199
|
+
command: null,
|
|
200
|
+
payload: {
|
|
201
|
+
...(suggestedBudget ? { suggested_provider_token_budget: suggestedBudget } : {}),
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
newRunAction(),
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
if (reasonCode === "retry_budget_exhausted") {
|
|
186
208
|
return [newRunAction()];
|
|
187
209
|
}
|
|
188
210
|
if (reasonCode === "local_execution_interrupted") {
|
package/src/runner.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
redactExecutionText,
|
|
20
20
|
} from "./orchestrator/execution-io.js";
|
|
21
21
|
import { summarizeCodingLoopEvent } from "./orchestrator/presentation.js";
|
|
22
|
-
import { refreshBudgetState } from "./orchestrator/budget.js";
|
|
22
|
+
import { increaseProviderTokenBudget, refreshBudgetState } from "./orchestrator/budget.js";
|
|
23
23
|
import { appendEvent } from "./orchestrator/shared.js";
|
|
24
24
|
import {
|
|
25
25
|
createProviderStreamDecoder,
|
|
@@ -252,6 +252,35 @@ export async function continueLocalCodingRun(run, guidance, dependencies = {}) {
|
|
|
252
252
|
return executeLocalCodingRun(run, dependencies, { humanGuidance: message, resume: true });
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
export async function increaseLocalCodingRunProviderTokenBudget(run, value) {
|
|
256
|
+
if (run.operation || run.child) {
|
|
257
|
+
throw new Error("Provider token budget can change only while the run is waiting.");
|
|
258
|
+
}
|
|
259
|
+
if (!RESUMABLE_STATES.has(run.status)) {
|
|
260
|
+
throw new Error("Provider token budget can change only for a blocked or waiting run.");
|
|
261
|
+
}
|
|
262
|
+
const currentBudget = refreshBudgetState(run.snapshot);
|
|
263
|
+
if (currentBudget.retry_exhausted) {
|
|
264
|
+
throw new Error("The Agentic Loop run also used its retry budget. Start a new run to continue.");
|
|
265
|
+
}
|
|
266
|
+
const previousBudget = currentBudget.token_budget;
|
|
267
|
+
const budget = increaseProviderTokenBudget(run.snapshot, value);
|
|
268
|
+
run.request.token_budget = budget.token_budget;
|
|
269
|
+
appendEvent(
|
|
270
|
+
run.snapshot,
|
|
271
|
+
"waiting_human",
|
|
272
|
+
`Provider token budget increased from ${previousBudget} to ${budget.token_budget}.`,
|
|
273
|
+
{
|
|
274
|
+
code: "provider_token_budget_increased",
|
|
275
|
+
previous_provider_token_budget: previousBudget,
|
|
276
|
+
provider_token_budget: budget.token_budget,
|
|
277
|
+
budget,
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
await updateRun(run, "waiting_human");
|
|
281
|
+
return budget;
|
|
282
|
+
}
|
|
283
|
+
|
|
255
284
|
export async function continueLocalCodingRunAfterRepositoryApproval(
|
|
256
285
|
run,
|
|
257
286
|
{ repository, localPath } = {},
|