@codespar/cli 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -1
- package/dist/commands/agent.js +13 -7
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/init.js +91 -23
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.js +2 -1
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/mandate-verify.js +75 -27
- package/dist/commands/mandate-verify.js.map +1 -1
- package/dist/config.js +17 -1
- package/dist/config.js.map +1 -1
- package/dist/did.js +262 -41
- package/dist/did.js.map +1 -1
- package/dist/index.js +23 -8
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/templates/bills-agent/LICENSE +21 -0
- package/templates/bills-agent/README.md +21 -0
- package/templates/bills-agent/_gitignore +10 -0
- package/templates/bills-agent/agents/bills-agent/.env.example +7 -0
- package/templates/bills-agent/agents/bills-agent/AGENTS.md +16 -0
- package/templates/bills-agent/agents/bills-agent/CLAUDE.md +16 -0
- package/templates/bills-agent/agents/bills-agent/README.md +89 -0
- package/templates/bills-agent/agents/bills-agent/SYSTEM_PROMPT.md +23 -0
- package/templates/bills-agent/agents/bills-agent/agent.yaml +26 -0
- package/templates/bills-agent/agents/bills-agent/channels/terminal/index.ts +108 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap-mandate.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap.json +10 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/exfiltration.json +13 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/false-authority.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/false-authority.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/fractioning.json +15 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/fractioning.transcript.jsonl +6 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/model-total.json +10 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/model-total.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/prompt-injection.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/prompt-injection.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/webhook-replay.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/eval.yaml +9 -0
- package/templates/bills-agent/agents/bills-agent/guardrails.json +15 -0
- package/templates/bills-agent/agents/bills-agent/mandate.example.json +46 -0
- package/templates/bills-agent/agents/bills-agent/package.json +24 -0
- package/templates/bills-agent/agents/bills-agent/runbook.md +19 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/beneficiary-not-allowed.json +14 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/cap-exceeded.json +12 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/cap-exceeded.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/escalated-above-threshold.json +20 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/escalated-above-threshold.transcript.jsonl +7 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/happy-path.json +14 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/happy-path.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/mandate-revoked.json +21 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/mandate-revoked.transcript.jsonl +9 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/prompt-injection.json +12 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/prompt-injection.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/src/adversarial.ts +177 -0
- package/templates/bills-agent/agents/bills-agent/src/bills.ts +28 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/approve.ts +9 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/check.ts +13 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/consent.ts +26 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/decide.ts +45 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/deny.ts +7 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/eval.ts +37 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/reconcile.ts +38 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/rerun.ts +65 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/resume.ts +30 -0
- package/templates/bills-agent/agents/bills-agent/src/main.ts +185 -0
- package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +125 -0
- package/templates/bills-agent/agents/bills-agent/src/modules/pix-out.ts +73 -0
- package/templates/bills-agent/agents/bills-agent/src/scenarios.ts +196 -0
- package/templates/bills-agent/agents/bills-agent/src/setup.ts +207 -0
- package/templates/bills-agent/agents/bills-agent/test/adversarial.test.ts +40 -0
- package/templates/bills-agent/agents/bills-agent/test/check.test.ts +85 -0
- package/templates/bills-agent/agents/bills-agent/test/cli.test.ts +177 -0
- package/templates/bills-agent/agents/bills-agent/test/scenarios.test.ts +73 -0
- package/templates/bills-agent/agents/bills-agent/tools.json +51 -0
- package/templates/bills-agent/agents/bills-agent/tsconfig.json +11 -0
- package/templates/bills-agent/package.json +34 -0
- package/templates/bills-agent/packages/agent-core/README.md +19 -0
- package/templates/bills-agent/packages/agent-core/package.json +24 -0
- package/templates/bills-agent/packages/agent-core/src/agent.ts +125 -0
- package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +154 -0
- package/templates/bills-agent/packages/agent-core/src/api/client.ts +53 -0
- package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +58 -0
- package/templates/bills-agent/packages/agent-core/src/api/rail.ts +101 -0
- package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +45 -0
- package/templates/bills-agent/packages/agent-core/src/approval.ts +109 -0
- package/templates/bills-agent/packages/agent-core/src/bundle.ts +113 -0
- package/templates/bills-agent/packages/agent-core/src/check.ts +171 -0
- package/templates/bills-agent/packages/agent-core/src/engine.ts +784 -0
- package/templates/bills-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/bills-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/bills-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/bills-agent/packages/agent-core/src/hash.ts +49 -0
- package/templates/bills-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/bills-agent/packages/agent-core/src/index.ts +29 -0
- package/templates/bills-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/bills-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/bills-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/bills-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/bills-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/bills-agent/packages/agent-core/src/rail.ts +87 -0
- package/templates/bills-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/bills-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/bills-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/bills-agent/packages/agent-core/src/state-machine.ts +146 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/charge-rail.ts +193 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +57 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +124 -0
- package/templates/bills-agent/packages/agent-core/src/tools.ts +55 -0
- package/templates/bills-agent/packages/agent-core/src/types.ts +106 -0
- package/templates/bills-agent/packages/agent-core/test/approval.test.ts +83 -0
- package/templates/bills-agent/packages/agent-core/test/engine.test.ts +524 -0
- package/templates/bills-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/bills-agent/packages/agent-core/test/helpers.ts +139 -0
- package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +153 -0
- package/templates/bills-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +265 -0
- package/templates/bills-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/bills-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/bills-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/bills-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/bills-agent/tsconfig.base.json +16 -0
- package/templates/bills-agent/vitest.config.ts +12 -0
- package/templates/collections-agent/LICENSE +21 -0
- package/templates/collections-agent/README.md +20 -0
- package/templates/collections-agent/_gitignore +10 -0
- package/templates/collections-agent/agents/collections-agent/.env.example +4 -0
- package/templates/collections-agent/agents/collections-agent/AGENTS.md +19 -0
- package/templates/collections-agent/agents/collections-agent/CLAUDE.md +19 -0
- package/templates/collections-agent/agents/collections-agent/README.md +94 -0
- package/templates/collections-agent/agents/collections-agent/SYSTEM_PROMPT.md +31 -0
- package/templates/collections-agent/agents/collections-agent/agent.yaml +28 -0
- package/templates/collections-agent/agents/collections-agent/channels/terminal/index.ts +178 -0
- package/templates/collections-agent/agents/collections-agent/channels/webhook/index.ts +131 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap-mandate.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/exfiltration.json +19 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/false-authority.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/false-authority.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/fractioning.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/fractioning.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/model-total.json +13 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/model-total.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/prompt-injection.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/prompt-injection.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/webhook-replay.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/eval.yaml +11 -0
- package/templates/collections-agent/agents/collections-agent/guardrails.json +20 -0
- package/templates/collections-agent/agents/collections-agent/mandate.example.json +40 -0
- package/templates/collections-agent/agents/collections-agent/package.json +29 -0
- package/templates/collections-agent/agents/collections-agent/runbook.md +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/beneficiary-not-allowed.json +42 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/cap-exceeded.json +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/cap-exceeded.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/charge-expired.json +36 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/charge-expired.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/escalated-above-threshold.json +49 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/escalated-above-threshold.transcript.jsonl +5 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/happy-path.json +60 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/happy-path.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/instalments.json +49 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/instalments.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/mandate-revoked.json +42 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/mandate-revoked.transcript.jsonl +6 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/prompt-injection.json +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/prompt-injection.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/src/adversarial.ts +190 -0
- package/templates/collections-agent/agents/collections-agent/src/agreements.ts +45 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/approve.ts +10 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/check.ts +13 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/decide.ts +48 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/deny.ts +7 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/eval.ts +39 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/poll.ts +35 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/reconcile.ts +38 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/rerun.ts +55 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/resume.ts +30 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/webhook.ts +36 -0
- package/templates/collections-agent/agents/collections-agent/src/envelope.ts +103 -0
- package/templates/collections-agent/agents/collections-agent/src/main.ts +193 -0
- package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +105 -0
- package/templates/collections-agent/agents/collections-agent/src/poll.ts +94 -0
- package/templates/collections-agent/agents/collections-agent/src/scenarios.ts +252 -0
- package/templates/collections-agent/agents/collections-agent/src/setup.ts +250 -0
- package/templates/collections-agent/agents/collections-agent/test/adversarial.test.ts +45 -0
- package/templates/collections-agent/agents/collections-agent/test/check.test.ts +51 -0
- package/templates/collections-agent/agents/collections-agent/test/cli.test.ts +172 -0
- package/templates/collections-agent/agents/collections-agent/test/envelope.test.ts +53 -0
- package/templates/collections-agent/agents/collections-agent/test/fixtures/accept-1042.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/test/fixtures/accept-1103.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/test/scenarios.test.ts +95 -0
- package/templates/collections-agent/agents/collections-agent/test/webhook.test.ts +100 -0
- package/templates/collections-agent/agents/collections-agent/tools.json +40 -0
- package/templates/collections-agent/agents/collections-agent/tsconfig.json +11 -0
- package/templates/collections-agent/package.json +35 -0
- package/templates/collections-agent/packages/agent-core/README.md +19 -0
- package/templates/collections-agent/packages/agent-core/package.json +24 -0
- package/templates/collections-agent/packages/agent-core/src/agent.ts +125 -0
- package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +154 -0
- package/templates/collections-agent/packages/agent-core/src/api/client.ts +53 -0
- package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +58 -0
- package/templates/collections-agent/packages/agent-core/src/api/rail.ts +101 -0
- package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +45 -0
- package/templates/collections-agent/packages/agent-core/src/approval.ts +109 -0
- package/templates/collections-agent/packages/agent-core/src/bundle.ts +113 -0
- package/templates/collections-agent/packages/agent-core/src/check.ts +171 -0
- package/templates/collections-agent/packages/agent-core/src/engine.ts +784 -0
- package/templates/collections-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/collections-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/collections-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/collections-agent/packages/agent-core/src/hash.ts +49 -0
- package/templates/collections-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/collections-agent/packages/agent-core/src/index.ts +29 -0
- package/templates/collections-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/collections-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/collections-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/collections-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/collections-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/collections-agent/packages/agent-core/src/rail.ts +87 -0
- package/templates/collections-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/collections-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/collections-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/collections-agent/packages/agent-core/src/state-machine.ts +146 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/charge-rail.ts +193 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +57 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +124 -0
- package/templates/collections-agent/packages/agent-core/src/tools.ts +55 -0
- package/templates/collections-agent/packages/agent-core/src/types.ts +106 -0
- package/templates/collections-agent/packages/agent-core/test/approval.test.ts +83 -0
- package/templates/collections-agent/packages/agent-core/test/engine.test.ts +524 -0
- package/templates/collections-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/collections-agent/packages/agent-core/test/helpers.ts +139 -0
- package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +153 -0
- package/templates/collections-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +265 -0
- package/templates/collections-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/collections-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/collections-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/collections-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/collections-agent/tsconfig.base.json +16 -0
- package/templates/collections-agent/vitest.config.ts +12 -0
- package/templates/kits.lock.json +41 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contracts that need a real process: `--json` on stdout and nothing
|
|
3
|
+
* else, the restart in `executing` followed by `resume`, and `rerun`.
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { DatabaseSync } from "node:sqlite";
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
|
|
12
|
+
const AGENT_DIR = resolve(import.meta.dirname, "..");
|
|
13
|
+
const NODE = process.execPath;
|
|
14
|
+
|
|
15
|
+
function run(script: string, args: string[], env: Record<string, string>) {
|
|
16
|
+
const result = spawnSync(NODE, ["--disable-warning=ExperimentalWarning", "--import", "tsx", script, ...args], {
|
|
17
|
+
cwd: AGENT_DIR,
|
|
18
|
+
env: { ...process.env, ANTHROPIC_API_KEY: "", CODESPAR_API_KEY: "", ...env },
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
timeout: 60_000,
|
|
21
|
+
});
|
|
22
|
+
return { code: result.status, stdout: result.stdout, stderr: result.stderr };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("npm start -- --input ... --json", () => {
|
|
26
|
+
it("prints valid JSON on stdout and nothing else; people go to stderr", () => {
|
|
27
|
+
const stateDir = mkdtempSync(join(tmpdir(), "bills-json-"));
|
|
28
|
+
const out = run("src/main.ts", ["--input", "pague a escola de outubro", "--approve", "--json"], { BILLS_STATE_DIR: stateDir, BILLS_RUNS_DIR: join(stateDir, "runs") });
|
|
29
|
+
expect(out.code).toBe(0);
|
|
30
|
+
const lines = out.stdout.split("\n").filter(Boolean);
|
|
31
|
+
expect(lines).toHaveLength(1);
|
|
32
|
+
const payload = JSON.parse(lines[0]!) as { run_id: string; mode: string; actor: { type: string }; executions: Array<{ state: string; receipt_ids: string[] }>; receipts: string[] };
|
|
33
|
+
expect(payload.mode).toBe("human");
|
|
34
|
+
expect(payload.actor.type).toBe("agent");
|
|
35
|
+
expect(payload.executions.map((e) => e.state)).toEqual(["settled"]);
|
|
36
|
+
expect(payload.receipts).toHaveLength(1);
|
|
37
|
+
expect(out.stderr).toContain("replay");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("refuses a key outside csk_test_ before anything else", () => {
|
|
41
|
+
const stateDir = mkdtempSync(join(tmpdir(), "bills-live-"));
|
|
42
|
+
const out = run("src/main.ts", ["--input", "pague a escola de outubro", "--json"], { BILLS_STATE_DIR: stateDir, CODESPAR_API_KEY: ["csk", "live", "0000000000"].join("_") });
|
|
43
|
+
expect(out.code).toBe(1);
|
|
44
|
+
expect(out.stdout).toBe("");
|
|
45
|
+
expect(out.stderr).toContain("csk_test_");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("section 10: restart in executing, then resume", () => {
|
|
50
|
+
it("one payment, one receipt, never two", () => {
|
|
51
|
+
const stateDir = mkdtempSync(join(tmpdir(), "bills-restart-"));
|
|
52
|
+
const runsDir = join(stateDir, "runs");
|
|
53
|
+
const env = { BILLS_STATE_DIR: stateDir, BILLS_RUNS_DIR: runsDir };
|
|
54
|
+
|
|
55
|
+
const killed = run("src/main.ts", ["--input", "pague a escola de outubro", "--approve", "--json"], { ...env, BILLS_KILL_AFTER_DISPATCH: "1" });
|
|
56
|
+
expect(killed.code).toBe(137);
|
|
57
|
+
expect(killed.stdout).toBe("");
|
|
58
|
+
|
|
59
|
+
const db = new DatabaseSync(join(stateDir, "state.db"));
|
|
60
|
+
const stuck = db.prepare("SELECT state FROM executions").all() as Array<{ state: string }>;
|
|
61
|
+
expect(stuck).toEqual([{ state: "executing" }]);
|
|
62
|
+
expect((db.prepare("SELECT COUNT(*) AS n FROM stub_rail_attempts").get() as { n: number }).n).toBe(1);
|
|
63
|
+
db.close();
|
|
64
|
+
|
|
65
|
+
const resumed = run("src/commands/resume.ts", ["--json"], env);
|
|
66
|
+
expect(resumed.code).toBe(0);
|
|
67
|
+
const payload = JSON.parse(resumed.stdout.trim()) as { resumed: Array<{ state: string; receipt_ids: string[] }> };
|
|
68
|
+
expect(payload.resumed).toHaveLength(1);
|
|
69
|
+
expect(payload.resumed[0]?.state).toBe("settled");
|
|
70
|
+
expect(payload.resumed[0]?.receipt_ids).toHaveLength(1);
|
|
71
|
+
|
|
72
|
+
const db2 = new DatabaseSync(join(stateDir, "state.db"));
|
|
73
|
+
expect((db2.prepare("SELECT COUNT(*) AS n FROM stub_rail_attempts").get() as { n: number }).n).toBe(1);
|
|
74
|
+
expect((db2.prepare("SELECT COUNT(*) AS n FROM events WHERE type = 'commerce.payment.succeeded'").get() as { n: number }).n).toBe(1);
|
|
75
|
+
expect(db2.prepare("SELECT state FROM executions").all()).toEqual([{ state: "settled" }]);
|
|
76
|
+
db2.close();
|
|
77
|
+
|
|
78
|
+
const receipts = readdirSync(runsDir).flatMap((r) => (existsSync(join(runsDir, r, "receipts")) ? readdirSync(join(runsDir, r, "receipts")) : []));
|
|
79
|
+
expect(receipts).toHaveLength(1);
|
|
80
|
+
|
|
81
|
+
const again = run("src/commands/resume.ts", ["--json"], env);
|
|
82
|
+
expect(JSON.parse(again.stdout.trim())).toMatchObject({ resumed: [] });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("npm run rerun <run-id>", () => {
|
|
87
|
+
it("reproduces a recorded run without network, with the same state sequence", () => {
|
|
88
|
+
const stateDir = mkdtempSync(join(tmpdir(), "bills-rerun-"));
|
|
89
|
+
const runsDir = join(stateDir, "runs");
|
|
90
|
+
const env = { BILLS_STATE_DIR: stateDir, BILLS_RUNS_DIR: runsDir };
|
|
91
|
+
const first = run("src/main.ts", ["--input", "pague a escola de outubro", "--approve", "--json"], env);
|
|
92
|
+
expect(first.code).toBe(0);
|
|
93
|
+
const { run_id } = JSON.parse(first.stdout.trim()) as { run_id: string };
|
|
94
|
+
const rerun = run("src/commands/rerun.ts", [run_id, "--json"], env);
|
|
95
|
+
expect(rerun.code).toBe(0);
|
|
96
|
+
const payload = JSON.parse(rerun.stdout.trim()) as { same_states: boolean; original: string[]; rerun: string[] };
|
|
97
|
+
expect(payload.same_states).toBe(true);
|
|
98
|
+
expect(payload.original).toEqual(["awaiting_approval", "approved", "executing", "settled"]);
|
|
99
|
+
expect(readFileSync(join(runsDir, run_id, "transcript.jsonl"), "utf8")).toContain("assistant_step");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("npm run check", () => {
|
|
104
|
+
it("is green for the shipped agent and prints JSON with --json", () => {
|
|
105
|
+
const out = run("src/commands/check.ts", ["--json"], {});
|
|
106
|
+
expect(out.code).toBe(0);
|
|
107
|
+
const report = JSON.parse(out.stdout.trim()) as { ok: boolean; agent: string; findings: unknown[] };
|
|
108
|
+
expect(report).toMatchObject({ ok: true, agent: "bills-agent" });
|
|
109
|
+
expect(report.findings.filter((f) => (f as { level: string }).level === "error")).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("npm run approve / deny <execution-id>", () => {
|
|
114
|
+
it("decides an execution left awaiting, produces the artifact and runs it through the last gate", () => {
|
|
115
|
+
const stateDir = mkdtempSync(join(tmpdir(), "bills-decide-"));
|
|
116
|
+
const runsDir = join(stateDir, "runs");
|
|
117
|
+
const env = { BILLS_STATE_DIR: stateDir, BILLS_RUNS_DIR: runsDir };
|
|
118
|
+
const left = run("src/main.ts", ["--input", "pague a escola de outubro", "--json"], env);
|
|
119
|
+
expect(left.code).toBe(0);
|
|
120
|
+
const payload = JSON.parse(left.stdout.trim()) as { executions: Array<{ id: string; state: string }> };
|
|
121
|
+
expect(payload.executions[0]?.state).toBe("awaiting_approval");
|
|
122
|
+
const id = payload.executions[0]!.id;
|
|
123
|
+
|
|
124
|
+
const denied = run("src/commands/deny.ts", ["nope_unknown", "--json"], env);
|
|
125
|
+
expect(denied.code).toBe(1);
|
|
126
|
+
|
|
127
|
+
const approved = run("src/commands/approve.ts", [id, "--user", "usr_titular", "--json"], env);
|
|
128
|
+
expect(approved.code).toBe(0);
|
|
129
|
+
const out = JSON.parse(approved.stdout.trim()) as { state: string; approval_id: string | null; receipt_ids: string[]; bundle_dir: string };
|
|
130
|
+
expect(out.state).toBe("settled");
|
|
131
|
+
expect(out.approval_id).toMatch(/^apr_/);
|
|
132
|
+
expect(out.receipt_ids).toHaveLength(1);
|
|
133
|
+
const artifacts = JSON.parse(readFileSync(join(AGENT_DIR, out.bundle_dir, "approval.json"), "utf8")) as Array<{ approver: { type: string; id: string } }>;
|
|
134
|
+
expect(artifacts[0]?.approver).toEqual({ type: "person", id: "usr_titular", channel: "terminal" });
|
|
135
|
+
|
|
136
|
+
const again = run("src/commands/approve.ts", [id, "--json"], env);
|
|
137
|
+
expect(again.code).toBe(1);
|
|
138
|
+
expect(again.stderr).toContain("not awaiting_approval");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("deny leaves a terminal denied execution and sends nothing", () => {
|
|
142
|
+
const stateDir = mkdtempSync(join(tmpdir(), "bills-deny-"));
|
|
143
|
+
const env = { BILLS_STATE_DIR: stateDir, BILLS_RUNS_DIR: join(stateDir, "runs") };
|
|
144
|
+
const left = run("src/main.ts", ["--input", "pague a escola de outubro", "--json"], env);
|
|
145
|
+
const id = (JSON.parse(left.stdout.trim()) as { executions: Array<{ id: string }> }).executions[0]!.id;
|
|
146
|
+
const denied = run("src/commands/deny.ts", [id, "--json"], env);
|
|
147
|
+
expect(denied.code).toBe(0);
|
|
148
|
+
expect(JSON.parse(denied.stdout.trim())).toMatchObject({ state: "denied", reason: "denied_by_approver", receipt_ids: [] });
|
|
149
|
+
const db = new DatabaseSync(join(stateDir, "state.db"));
|
|
150
|
+
expect((db.prepare("SELECT COUNT(*) AS n FROM stub_rail_attempts").get() as { n: number }).n).toBe(0);
|
|
151
|
+
db.close();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("partial failure of a multi-item execution, and rerun reproducing it", () => {
|
|
156
|
+
it("names what settled and what failed, and rerun replays the rail's refusal", () => {
|
|
157
|
+
const stateDir = mkdtempSync(join(tmpdir(), "bills-partial-"));
|
|
158
|
+
const runsDir = join(stateDir, "runs");
|
|
159
|
+
const env = { BILLS_STATE_DIR: stateDir, BILLS_RUNS_DIR: runsDir, BILLS_STUB_REFUSE: "+5511999990001" };
|
|
160
|
+
const first = run("src/main.ts", ["--input", "libera o lote do mes", "--transcript", "evals/adversarial/false-authority.transcript.jsonl", "--approve", "--json"], env);
|
|
161
|
+
expect(first.code).toBe(0);
|
|
162
|
+
const payload = JSON.parse(first.stdout.trim()) as { run_id: string; executions: Array<{ state: string; receipt_ids: string[] }>; receipts: string[] };
|
|
163
|
+
expect(payload.executions[0]?.state).toBe("failed");
|
|
164
|
+
expect(payload.executions[0]?.receipt_ids).toHaveLength(1);
|
|
165
|
+
expect(payload.receipts).toHaveLength(1);
|
|
166
|
+
const events = readFileSync(join(runsDir, payload.run_id, "events.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l) as { type: string; payload: { status?: string } });
|
|
167
|
+
expect(events.filter((e) => e.type === "rail.outcome").map((e) => e.payload.status)).toEqual(["settled", "failed"]);
|
|
168
|
+
|
|
169
|
+
const rerun = run("src/commands/rerun.ts", [payload.run_id, "--json"], { BILLS_STATE_DIR: stateDir, BILLS_RUNS_DIR: runsDir });
|
|
170
|
+
expect(rerun.code).toBe(0);
|
|
171
|
+
const r = JSON.parse(rerun.stdout.trim()) as { same_states: boolean; original: string[]; rerun: string[]; original_outcomes: string[]; rerun_outcomes: string[] };
|
|
172
|
+
expect(r.same_states).toBe(true);
|
|
173
|
+
expect(r.original).toEqual(["awaiting_approval", "approved", "executing", "failed"]);
|
|
174
|
+
expect(r.original_outcomes).toEqual(["settled", "failed"]);
|
|
175
|
+
expect(r.rerun_outcomes).toEqual(["settled", "failed"]);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { checkScenario, listScenarios, loadScenario, runScenario, type ScenarioRun } from "../src/scenarios.js";
|
|
6
|
+
|
|
7
|
+
const runsDir = mkdtempSync(join(tmpdir(), "bills-runs-"));
|
|
8
|
+
|
|
9
|
+
describe("section 12: scenario packs, on the replay provider", () => {
|
|
10
|
+
const required = ["happy-path", "cap-exceeded", "beneficiary-not-allowed", "prompt-injection", "escalated-above-threshold", "mandate-revoked"];
|
|
11
|
+
|
|
12
|
+
it("ships every scenario the spec asks for", () => {
|
|
13
|
+
for (const name of required) expect(listScenarios()).toContain(name);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
for (const name of required) {
|
|
17
|
+
const scenario = loadScenario(name);
|
|
18
|
+
for (const mode of scenario.modes) {
|
|
19
|
+
it(`${name} [${mode}] ends in the states the pack declares`, async () => {
|
|
20
|
+
const run = await runScenario(scenario, { mode, runsDir });
|
|
21
|
+
const check = checkScenario(scenario, run);
|
|
22
|
+
expect(check.failures).toEqual([]);
|
|
23
|
+
expect(check.ok).toBe(true);
|
|
24
|
+
// Every event of the bundle carries an actor.
|
|
25
|
+
const events = readFileSync(join(run.bundle_dir, "events.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l) as { actor?: unknown });
|
|
26
|
+
expect(events.length).toBeGreaterThan(0);
|
|
27
|
+
for (const e of events) expect(e.actor).toBeDefined();
|
|
28
|
+
// Every receipt carries an actor and no raw payee.
|
|
29
|
+
for (const file of readdirSync(join(run.bundle_dir, "receipts"))) {
|
|
30
|
+
const receipt = JSON.parse(readFileSync(join(run.bundle_dir, "receipts", file), "utf8")) as { actor?: unknown; payment: { payee: string | null } };
|
|
31
|
+
expect(receipt.actor).toBeDefined();
|
|
32
|
+
expect(receipt.payment.payee ?? "").toContain("***");
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
it("happy-path: the same trail and the same receipts in human and in mandate", async () => {
|
|
39
|
+
const scenario = loadScenario("happy-path");
|
|
40
|
+
const human = await runScenario(scenario, { mode: "human", runsDir });
|
|
41
|
+
const mandate = await runScenario(scenario, { mode: "mandate", runsDir });
|
|
42
|
+
expect(human.executions.map((e) => e.trail)).toEqual(mandate.executions.map((e) => e.trail));
|
|
43
|
+
expect(normalizeReceipts(human)).toEqual(normalizeReceipts(mandate));
|
|
44
|
+
// In human, the artifact is a person's; in mandate the person still decided because the amount escalated.
|
|
45
|
+
const approvals = (run: ScenarioRun) => JSON.parse(readFileSync(join(run.bundle_dir, "approval.json"), "utf8")) as Array<{ approver: { type: string }; items_hash: string; escalation?: { trigger: string } }>;
|
|
46
|
+
expect(approvals(human)[0]?.approver.type).toBe("person");
|
|
47
|
+
expect(approvals(mandate)[0]?.approver.type).toBe("person");
|
|
48
|
+
expect(approvals(mandate)[0]?.escalation?.trigger).toBe("amount");
|
|
49
|
+
expect(approvals(human)[0]?.items_hash).toBe(approvals(mandate)[0]?.items_hash);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("nothing reaches executing without an approval artifact carrying its items_hash", async () => {
|
|
53
|
+
for (const name of required) {
|
|
54
|
+
const scenario = loadScenario(name);
|
|
55
|
+
for (const mode of scenario.modes) {
|
|
56
|
+
const run = await runScenario(scenario, { mode, runsDir });
|
|
57
|
+
const approvalPath = join(run.bundle_dir, "approval.json");
|
|
58
|
+
const artifacts = existsSync(approvalPath) ? (JSON.parse(readFileSync(approvalPath, "utf8")) as Array<{ execution_id: string; items_hash: string }>) : [];
|
|
59
|
+
for (const e of run.executions) {
|
|
60
|
+
if (!e.trail.includes("executing")) continue;
|
|
61
|
+
expect(artifacts.some((a) => a.execution_id === e.id)).toBe(true);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function normalizeReceipts(run: ScenarioRun): unknown[] {
|
|
69
|
+
return readdirSync(join(run.bundle_dir, "receipts"))
|
|
70
|
+
.map((f) => JSON.parse(readFileSync(join(run.bundle_dir, "receipts", f), "utf8")) as { state: string; mandate: { id: string }; payment: { amount_minor: number; payee: string | null; sandbox: boolean } })
|
|
71
|
+
.map((r) => ({ state: r.state, mandate: r.mandate.id, amount: r.payment.amount_minor, payee: r.payment.payee, sandbox: r.payment.sandbox }))
|
|
72
|
+
.sort((a, b) => a.amount - b.amount);
|
|
73
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta_tools": [
|
|
3
|
+
{
|
|
4
|
+
"name": "codespar_pay",
|
|
5
|
+
"effect": "payment",
|
|
6
|
+
"description": "Propose one or more Pix payments to named payees of the month. This does NOT pay: it creates an execution in `drafted` that the core checks against the mandate and, depending on the approval mode, asks the person or runs. Use the payee alias from list_bills (escola, mercado, funcionaria, contas). Amounts are in BRL cents.",
|
|
7
|
+
"input_schema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"action": { "type": "string", "enum": ["pix"] },
|
|
11
|
+
"items": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"minItems": 1,
|
|
14
|
+
"items": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"properties": {
|
|
17
|
+
"payee": { "type": "string", "description": "A payee alias from list_bills. Never a raw Pix key the person typed in the chat." },
|
|
18
|
+
"amount_minor": { "type": "integer", "minimum": 1, "description": "BRL cents." },
|
|
19
|
+
"description": { "type": "string" }
|
|
20
|
+
},
|
|
21
|
+
"required": ["payee", "amount_minor"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"total_minor": { "type": "integer", "description": "Optional. What you believe the total is. The core recomputes it and yours is only recorded." }
|
|
25
|
+
},
|
|
26
|
+
"required": ["action", "items"]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "codespar_ledger",
|
|
31
|
+
"effect": "read",
|
|
32
|
+
"description": "Read the receipt of a settled payment by receipt id, or list the executions of this run with their states.",
|
|
33
|
+
"input_schema": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"action": { "type": "string", "enum": ["receipt", "executions"] },
|
|
37
|
+
"receipt_id": { "type": "string" }
|
|
38
|
+
},
|
|
39
|
+
"required": ["action"]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"local_tools": [
|
|
44
|
+
{
|
|
45
|
+
"name": "list_bills",
|
|
46
|
+
"effect": "read",
|
|
47
|
+
"description": "The bills of the current month: payee alias, name, amount in BRL cents, due date and whether it was already paid in this mandate window. Read-only.",
|
|
48
|
+
"input_schema": { "type": "object", "properties": {} }
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "The consumer delegates the month's bills to an agent that pays under a signed mandate: per-payment cap, monthly cap, named payees, expiry. Every payment returns a receipt.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"workspaces": [
|
|
9
|
+
"packages/agent-core",
|
|
10
|
+
"agents/bills-agent"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "npm start --workspace=agents/bills-agent --",
|
|
14
|
+
"check": "npm run check --workspaces --if-present --",
|
|
15
|
+
"test": "vitest run --pool=forks --maxWorkers=1",
|
|
16
|
+
"typecheck": "tsc --noEmit -p packages/agent-core/tsconfig.json && tsc --noEmit -p agents/bills-agent/tsconfig.json",
|
|
17
|
+
"approve": "npm run approve --workspace=agents/bills-agent --",
|
|
18
|
+
"consent": "npm run consent --workspace=agents/bills-agent --",
|
|
19
|
+
"deny": "npm run deny --workspace=agents/bills-agent --",
|
|
20
|
+
"eval": "npm run eval --workspace=agents/bills-agent --",
|
|
21
|
+
"reconcile": "npm run reconcile --workspace=agents/bills-agent --",
|
|
22
|
+
"rerun": "npm run rerun --workspace=agents/bills-agent --",
|
|
23
|
+
"resume": "npm run resume --workspace=agents/bills-agent --"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "22.20.4",
|
|
27
|
+
"tsx": "4.20.5",
|
|
28
|
+
"typescript": "5.9.2",
|
|
29
|
+
"vitest": "3.2.4"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22.13"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @codespar/agent-core
|
|
2
|
+
|
|
3
|
+
The primitives every CodeSpar starter-kit agent inherits (spec v5.1.1, section 4): the execution state machine, the approval artifact, the `agent.yaml` manifest, `escalate_above`, `actor`, local state in SQLite, the proof bundle, the rail contract and the two providers.
|
|
4
|
+
|
|
5
|
+
## The state machine, and what the types do and do not prove
|
|
6
|
+
|
|
7
|
+
`transition(execution, to, input)` accepts only a `to` that the table in `state-machine.ts` lists for `execution.state`. On a **narrowed** value (`Execution<"drafted">`, `Execution<"executing">`, ...) a transition outside the table is a compile error, and `test/transitions.type-test.ts` keeps that true with `@ts-expect-error` lines that `tsc` refuses to leave unused. An **un-narrowed** `Execution` (the union, which is what a row read back from SQLite is) compiles for any target; the engine narrows with casts at its call sites. So the guard that always holds is the runtime one: `transition()` throws `IllegalTransitionError` on any pair the table does not list, whatever the static type said. The type test proves the table is closed; the runtime check enforces it.
|
|
8
|
+
|
|
9
|
+
## The one policy, three gates
|
|
10
|
+
|
|
11
|
+
`ExecutionEngine.policy()` runs at draft, at a human's approval and again immediately before `executing`, inside the transaction that writes the outbox row. Mandate status, allowlist, per-transaction cap, the window cap (counting settled, in flight AND awaiting executions as reservations), `escalate_above` and the artifact's `items_hash` are all re-checked at the last gate. A person's approval satisfies `escalate_above` and nothing else.
|
|
12
|
+
|
|
13
|
+
## Reconcile never re-dispatches
|
|
14
|
+
|
|
15
|
+
An execution left in `executing` is looked up on the rail, attempt by attempt. A recorded settled or failed outcome closes it; `in_flight`, `uncertain` or unknown leaves it in `executing` with `reason: rail_uncertain` for a human. The single exception is an outbox row still `pending` (the state changed, no call was ever made): those attempts go out once, under the same ids.
|
|
16
|
+
|
|
17
|
+
## Stubs
|
|
18
|
+
|
|
19
|
+
`stubs/mandate-status.ts` (the section 4.7 status source for runs without a key: revocation, pause and the organization kill switch, over the local state.db) and `stubs/rail.ts` (the sandbox rail) are stand-ins for the CI, the scenarios and `rerun`, marked as such. With a test key the engine reads the mandate status from the API instead (`api/mandate-status.ts`, `GET /v1/mandates/{id}`), fail-closed: anything but `active` refuses `executing`, and a read that does not answer is `mandate_status_unavailable`, never "assume active". The approval artifact is signed with a local development key (`approval.ts`); the API does not sign approval lists.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codespar/agent-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The primitives every CodeSpar starter-kit agent inherits: the execution state machine, the approval artifact, the agent.yaml manifest, escalate_above, actor, local state and providers.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.ts",
|
|
11
|
+
"./providers/anthropic": "./src/providers/anthropic.ts",
|
|
12
|
+
"./providers/replay": "./src/providers/replay.ts"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
16
|
+
"test": "vitest run --pool=forks --maxWorkers=1 --root ../.. packages/agent-core"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@anthropic-ai/sdk": "0.128.0",
|
|
20
|
+
"@codespar/sdk": "0.16.4",
|
|
21
|
+
"yaml": "2.9.1",
|
|
22
|
+
"zod": "3.25.76"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool-use loop. The provider says what the model wants; this file
|
|
3
|
+
* dispatches only the tools `tools.json` names, records every step in the
|
|
4
|
+
* transcript, and hands payment tools to the engine, which is the only
|
|
5
|
+
* thing that can move an execution past `drafted`.
|
|
6
|
+
*/
|
|
7
|
+
import type { ProofBundle } from "./bundle.js";
|
|
8
|
+
import type { ExecutionEngine } from "./engine.js";
|
|
9
|
+
import type { AgentRuntime, ConversationMessage, ToolCall, ToolSpec } from "./providers/types.js";
|
|
10
|
+
import { isReply } from "./providers/types.js";
|
|
11
|
+
import type { Execution } from "./state-machine.js";
|
|
12
|
+
import type { ToolsFile } from "./tools.js";
|
|
13
|
+
import { allowedToolNames, toolSpecs } from "./tools.js";
|
|
14
|
+
|
|
15
|
+
export interface ToolContext {
|
|
16
|
+
engine: ExecutionEngine;
|
|
17
|
+
/** Fires when a payment tool created an execution; the channel decides what to do with it (ask, or run). */
|
|
18
|
+
onExecution: (execution: Execution) => Promise<Execution>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ToolHandler = (input: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
|
|
22
|
+
|
|
23
|
+
export interface AgentLoopOptions {
|
|
24
|
+
runtime: AgentRuntime;
|
|
25
|
+
tools: ToolsFile;
|
|
26
|
+
handlers: Record<string, ToolHandler>;
|
|
27
|
+
system: string;
|
|
28
|
+
bundle: ProofBundle;
|
|
29
|
+
engine: ExecutionEngine;
|
|
30
|
+
onExecution: ToolContext["onExecution"];
|
|
31
|
+
/** A hard stop on tool round-trips per turn, so a looping model cannot run forever. */
|
|
32
|
+
maxSteps?: number;
|
|
33
|
+
clock?: () => Date;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TurnResult {
|
|
37
|
+
reply: string;
|
|
38
|
+
tool_calls: Array<{ name: string; refused: boolean }>;
|
|
39
|
+
executions: Execution[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class AgentLoop {
|
|
43
|
+
private readonly messages: ConversationMessage[] = [];
|
|
44
|
+
private readonly allowed: Set<string>;
|
|
45
|
+
private readonly specs: ToolSpec[];
|
|
46
|
+
private readonly clock: () => Date;
|
|
47
|
+
|
|
48
|
+
constructor(private readonly options: AgentLoopOptions) {
|
|
49
|
+
this.allowed = allowedToolNames(options.tools);
|
|
50
|
+
this.specs = toolSpecs(options.tools);
|
|
51
|
+
this.clock = options.clock ?? (() => new Date());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get history(): readonly ConversationMessage[] {
|
|
55
|
+
return this.messages;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async turn(userText: string): Promise<TurnResult> {
|
|
59
|
+
const { runtime, bundle } = this.options;
|
|
60
|
+
const maxSteps = this.options.maxSteps ?? 8;
|
|
61
|
+
const calls: TurnResult["tool_calls"] = [];
|
|
62
|
+
const executions: Execution[] = [];
|
|
63
|
+
|
|
64
|
+
this.messages.push({ role: "user", content: userText });
|
|
65
|
+
bundle.transcript({ at: this.now(), kind: "user", text: userText });
|
|
66
|
+
|
|
67
|
+
for (let step = 0; step < maxSteps; step += 1) {
|
|
68
|
+
const out = await runtime.step({ system: this.options.system, messages: this.messages }, this.specs);
|
|
69
|
+
if (isReply(out)) {
|
|
70
|
+
this.messages.push({ role: "assistant", content: out.text });
|
|
71
|
+
bundle.transcript({ at: this.now(), kind: "assistant_step", reply: out.text });
|
|
72
|
+
bundle.transcript({ at: this.now(), kind: "assistant", text: out.text });
|
|
73
|
+
return { reply: out.text, tool_calls: calls, executions };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.messages.push({ role: "assistant", content: "", tool_calls: out });
|
|
77
|
+
bundle.transcript({ at: this.now(), kind: "assistant_step", tool_calls: out });
|
|
78
|
+
|
|
79
|
+
for (const call of out) {
|
|
80
|
+
const result = await this.dispatch(call, executions);
|
|
81
|
+
calls.push({ name: call.name, refused: result.refused });
|
|
82
|
+
this.messages.push({ role: "tool", tool_call_id: call.id, name: call.name, content: JSON.stringify(result.content), ...(result.refused ? { is_error: true } : {}) });
|
|
83
|
+
bundle.transcript({ at: this.now(), kind: "tool_result", tool_call_id: call.id, name: call.name, refused: result.refused, content: result.content });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const text = "I stopped: too many tool steps in one turn. Nothing was paid beyond what the trail shows.";
|
|
88
|
+
this.messages.push({ role: "assistant", content: text });
|
|
89
|
+
bundle.transcript({ at: this.now(), kind: "assistant", text, truncated: true });
|
|
90
|
+
return { reply: text, tool_calls: calls, executions };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async dispatch(call: ToolCall, executions: Execution[]): Promise<{ refused: boolean; content: unknown }> {
|
|
94
|
+
const { bundle, handlers, engine, onExecution } = this.options;
|
|
95
|
+
bundle.transcript({ at: this.now(), kind: "tool_call", tool_call_id: call.id, name: call.name, input: call.input });
|
|
96
|
+
|
|
97
|
+
if (!this.allowed.has(call.name)) {
|
|
98
|
+
// Section 15, closed tools: a tool outside tools.json is refused, and the refusal is in the trail.
|
|
99
|
+
bundle.event({ at: this.now(), type: "tool.refused", tool: call.name, reason: "tool_not_allowed", actor: engine.agentActor });
|
|
100
|
+
return { refused: true, content: { error: "tool_not_allowed", message: `${call.name} is not in this agent's tools.json` } };
|
|
101
|
+
}
|
|
102
|
+
const handler = handlers[call.name];
|
|
103
|
+
if (!handler) return { refused: true, content: { error: "tool_not_implemented", message: `${call.name} has no handler` } };
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const content = await handler(call.input, {
|
|
107
|
+
engine,
|
|
108
|
+
onExecution: async (execution) => {
|
|
109
|
+
const settledOrNot = await onExecution(execution);
|
|
110
|
+
executions.push(settledOrNot);
|
|
111
|
+
return settledOrNot;
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
return { refused: false, content };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
117
|
+
bundle.event({ at: this.now(), type: "tool.error", tool: call.name, message, actor: engine.agentActor });
|
|
118
|
+
return { refused: true, content: { error: "tool_failed", message } };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private now(): string {
|
|
123
|
+
return this.clock().toISOString();
|
|
124
|
+
}
|
|
125
|
+
}
|