@codespar/cli 0.13.0 → 0.14.0
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/index.js +6 -3
- 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,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm run rerun <run-id> [--json]`: replays a recorded run against the
|
|
3
|
+
* deterministic provider and the stub rail, with no network, in a fresh
|
|
4
|
+
* state, and compares the sequence of states with the original bundle. The
|
|
5
|
+
* model's outputs are the recording; the payer's behaviour is read from the
|
|
6
|
+
* recorded outcomes (paid, expired); every decision of the core is recomputed.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdtempSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { stderr, stdout } from "node:process";
|
|
12
|
+
import { ProofBundle, type ApprovalMode, type Execution } from "@codespar/agent-core";
|
|
13
|
+
import { handleExecution } from "../../channels/terminal/index.js";
|
|
14
|
+
import { runsDir, setup } from "../setup.js";
|
|
15
|
+
|
|
16
|
+
const argv = process.argv.slice(2);
|
|
17
|
+
const json = argv.includes("--json");
|
|
18
|
+
const runId = argv.find((a) => !a.startsWith("--"));
|
|
19
|
+
const say = (l: string) => stderr.write(l + "\n");
|
|
20
|
+
if (!runId) {
|
|
21
|
+
say("usage: npm run rerun <run-id> [--json]");
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
const original = ProofBundle.open(runsDir(), runId);
|
|
25
|
+
if (!original) {
|
|
26
|
+
say(`no bundle at runs/${runId}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const meta = original.readMeta() ?? {};
|
|
30
|
+
const mode = (meta["mode"] as ApprovalMode | undefined) ?? "human";
|
|
31
|
+
const events = original.readEvents();
|
|
32
|
+
const originalTrail = events.filter((e) => e["type"] === "execution.transition").map((e) => (e["payload"] as { to: string }).to);
|
|
33
|
+
const originalDecisions = events.filter((e) => e["type"] === "approval.created").map((e) => ((e["payload"] as { approver: { type: string } }).approver.type === "person" ? "approve" : "mandate"));
|
|
34
|
+
const userTurns = original.readTranscript().filter((l) => l.kind === "user").map((l) => l["text"] as string);
|
|
35
|
+
// The payer's behaviour is part of the recording: a receivable that expired in the original run expires in the rerun.
|
|
36
|
+
const expired = events.some((e) => e["type"] === "execution.transition" && (e["payload"] as { reason?: string }).reason === "charge_expired");
|
|
37
|
+
const transcriptPath = join(original.dir, "transcript.jsonl");
|
|
38
|
+
|
|
39
|
+
const s = setup({ mode, rail: "stub", provider: "replay", transcript: transcriptPath, runId: `${runId}_rerun_${Date.now().toString(36)}`, stateDir: mkdtempSync(join(tmpdir(), "collections-rerun-")), stubRail: { payer: expired ? "expires" : "pays" }, say });
|
|
40
|
+
try {
|
|
41
|
+
let decisionIndex = 0;
|
|
42
|
+
const runtime = s.makeRuntime();
|
|
43
|
+
const loop = s.makeLoop(runtime, (execution: Execution) => {
|
|
44
|
+
const decision = execution.state === "awaiting_approval" ? (originalDecisions[decisionIndex++] === "approve" ? "approve" : "deny") : "none";
|
|
45
|
+
return handleExecution(execution, { setup: s, approver: { id: "usr_rerun", channel: "terminal" }, decision, say, tell: () => undefined, simulatePayer: !expired });
|
|
46
|
+
});
|
|
47
|
+
for (const text of userTurns) await loop.turn(text);
|
|
48
|
+
const trail = s.store.listEvents({ run_id: s.runId }).filter((e) => e.type === "execution.transition").map((e) => (e.payload as { to: string }).to);
|
|
49
|
+
const same = JSON.stringify(trail) === JSON.stringify(originalTrail);
|
|
50
|
+
if (json) stdout.write(JSON.stringify({ run_id: runId, rerun_id: s.runId, same_states: same, original: originalTrail, rerun: trail, bundle_dir: s.bundle.dir }) + "\n");
|
|
51
|
+
say(same ? `rerun ok: ${trail.length} transition(s), same sequence as ${runId}` : `rerun DIFFERS: original ${JSON.stringify(originalTrail)} vs rerun ${JSON.stringify(trail)}`);
|
|
52
|
+
process.exit(same ? 0 : 1);
|
|
53
|
+
} finally {
|
|
54
|
+
s.close();
|
|
55
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm run resume [--json]`: after a crash or a restart. Executions left in
|
|
3
|
+
* `executing` are reconciled against the rail (idempotent on attempt_id),
|
|
4
|
+
* never repeated: a receivable already issued is looked at, not re-issued;
|
|
5
|
+
* one whose outbox is still pending goes out once. Stale open ones expire.
|
|
6
|
+
*/
|
|
7
|
+
import { stderr, stdout } from "node:process";
|
|
8
|
+
import { readDotEnv, setup } from "../setup.js";
|
|
9
|
+
|
|
10
|
+
const json = process.argv.includes("--json");
|
|
11
|
+
const say = (l: string) => stderr.write(l + "\n");
|
|
12
|
+
readDotEnv();
|
|
13
|
+
const s = setup({ say, runId: `run_resume_${Date.now().toString(36)}` });
|
|
14
|
+
try {
|
|
15
|
+
const resumed = [];
|
|
16
|
+
for (const stuck of s.engine.list({ state: "executing" })) {
|
|
17
|
+
const closed = await s.engine.resumePending(stuck.id);
|
|
18
|
+
resumed.push({ id: closed.id, state: closed.state, reason: closed.reason ?? null, detail: closed.detail ?? null, charge_ids: closed.outcomes.map((o) => o.transaction_id ?? null), receipt_ids: closed.outcomes.filter((o) => o.receipt_id).map((o) => o.receipt_id) });
|
|
19
|
+
say(closed.state === "executing" ? `${closed.id}: still executing (${closed.reason}) — ${closed.detail}; nothing was re-issued; \`npm run poll\` keeps looking` : `${closed.id}: executing -> ${closed.state}`);
|
|
20
|
+
}
|
|
21
|
+
const expired = s.engine.expireStale().map((e) => ({ id: e.id, state: e.state }));
|
|
22
|
+
for (const e of expired) say(`${e.id}: -> expired`);
|
|
23
|
+
const open = s.engine.list({ state: ["awaiting_approval", "approved"] }).map((e) => ({ id: e.id, state: e.state }));
|
|
24
|
+
for (const e of open) say(`${e.id}: still ${e.state}; decide it with npm run approve ${e.id} / npm run deny ${e.id}`);
|
|
25
|
+
if (json) stdout.write(JSON.stringify({ resumed, expired, open, bundle_dir: s.bundle.dir }) + "\n");
|
|
26
|
+
else say(resumed.length || expired.length ? "resume done" : "nothing to resume");
|
|
27
|
+
process.exit(resumed.some((r) => r.state === "executing" && r.reason !== "awaiting_settlement") ? 3 : 0);
|
|
28
|
+
} finally {
|
|
29
|
+
s.close();
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm run webhook [--port <n>] [--secret <trigger secret>]`: starts the
|
|
3
|
+
* receiving end of `channels/webhook` on localhost. A stub for the
|
|
4
|
+
* developer who has a URL the API can reach; the terminal's default is
|
|
5
|
+
* `npm run poll`. Reads `COLLECTIONS_WEBHOOK_PORT` and
|
|
6
|
+
* `COLLECTIONS_TRIGGER_SECRET` when the flags are absent.
|
|
7
|
+
*/
|
|
8
|
+
import { stderr, stdout } from "node:process";
|
|
9
|
+
import { announceOutcome } from "../../channels/terminal/index.js";
|
|
10
|
+
import { startWebhookServer } from "../../channels/webhook/index.js";
|
|
11
|
+
import { readDotEnv, setup } from "../setup.js";
|
|
12
|
+
|
|
13
|
+
const argv = process.argv.slice(2);
|
|
14
|
+
readDotEnv();
|
|
15
|
+
const port = Number(argv.includes("--port") ? argv[argv.indexOf("--port") + 1] : process.env["COLLECTIONS_WEBHOOK_PORT"] ?? "8787");
|
|
16
|
+
const secret = argv.includes("--secret") ? argv[argv.indexOf("--secret") + 1] : process.env["COLLECTIONS_TRIGGER_SECRET"];
|
|
17
|
+
const say = (l: string) => stderr.write(l + "\n");
|
|
18
|
+
const s = setup({ say, runId: `run_webhook_${Date.now().toString(36)}` });
|
|
19
|
+
const server = startWebhookServer(
|
|
20
|
+
port,
|
|
21
|
+
{
|
|
22
|
+
engine: s.engine,
|
|
23
|
+
secret,
|
|
24
|
+
onClosed: (execution) => {
|
|
25
|
+
announceOutcome(execution, s, (l) => stdout.write(l + "\n"));
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
say,
|
|
29
|
+
);
|
|
30
|
+
const stop = () => {
|
|
31
|
+
server.close();
|
|
32
|
+
s.close();
|
|
33
|
+
process.exit(0);
|
|
34
|
+
};
|
|
35
|
+
process.on("SIGINT", stop);
|
|
36
|
+
process.on("SIGTERM", stop);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module `bolepix-receivables`, the envelope: what the agent may agree on
|
|
3
|
+
* its own, applied by deterministic code at every gate of the core. The
|
|
4
|
+
* model negotiates inside it; the code confers before the proposal leaves
|
|
5
|
+
* (section 7, step 2). The policy of the receiving side is not signed by
|
|
6
|
+
* the API today (section 16), so it lives in `guardrails.json` under
|
|
7
|
+
* `envelope` and the core runs it as its `policyExtension`.
|
|
8
|
+
*
|
|
9
|
+
* Every rule here can only refuse. None of them widens the collection
|
|
10
|
+
* policy (the debtors' book, the cap per receivable, the window cap).
|
|
11
|
+
*/
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { isOutsideHours, localClock, type Execution, type ExecutionReason, type Guardrails, type PolicyExtension } from "@codespar/agent-core";
|
|
14
|
+
import { agreementByDocument, type Agreement } from "./agreements.js";
|
|
15
|
+
|
|
16
|
+
export const EnvelopeSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
/** The largest discount over the principal, in percent. */
|
|
19
|
+
max_discount_pct: z.number().min(0).max(100),
|
|
20
|
+
max_instalments: z.number().int().positive(),
|
|
21
|
+
/** A due date may be at most this many days from today. */
|
|
22
|
+
due_date_window_days: z.number().int().positive(),
|
|
23
|
+
min_instalment_minor: z.number().int().positive(),
|
|
24
|
+
/** The hours a debtor may be contacted, `HH:MM-HH:MM` in `guardrails.timezone`. Outside them nothing is proposed or issued. */
|
|
25
|
+
collection_hours: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d-([01]\d|2[0-3]):[0-5]\d$/),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
|
|
29
|
+
export type Envelope = z.infer<typeof EnvelopeSchema>;
|
|
30
|
+
|
|
31
|
+
export function loadEnvelope(guardrails: Guardrails): Envelope {
|
|
32
|
+
if (!guardrails.envelope) throw new Error("guardrails.json has no `envelope`; the collections-agent needs one");
|
|
33
|
+
return EnvelopeSchema.parse(guardrails.envelope);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The floor of an agreement: the principal minus the maximum discount, rounded up so the discount never exceeds the ceiling by a cent. */
|
|
37
|
+
export function floorMinor(agreement: Agreement, envelope: Envelope): number {
|
|
38
|
+
return Math.ceil((agreement.principal_minor * (100 - envelope.max_discount_pct)) / 100);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `YYYY-MM-DD` of `now` in the zone. */
|
|
42
|
+
export function localDate(now: Date, timezone: string): string {
|
|
43
|
+
const parts = new Intl.DateTimeFormat("en-CA", { timeZone: timezone, year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(now);
|
|
44
|
+
const get = (t: string) => parts.find((p) => p.type === t)?.value ?? "";
|
|
45
|
+
return `${get("year")}-${get("month")}-${get("day")}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function addDays(iso: string, days: number): string {
|
|
49
|
+
const d = new Date(`${iso}T00:00:00Z`);
|
|
50
|
+
d.setUTCDate(d.getUTCDate() + days);
|
|
51
|
+
return d.toISOString().slice(0, 10);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface EnvelopeVerdict {
|
|
55
|
+
reason: ExecutionReason;
|
|
56
|
+
detail: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The pure check, for tests and for the tool handler's early answer. `undefined` means inside the envelope. */
|
|
60
|
+
export function checkEnvelope(execution: Pick<Execution, "items" | "total">, envelope: Envelope, now: Date, timezone: string): EnvelopeVerdict | undefined {
|
|
61
|
+
// Collection hours are the law's, not the merchant's: the window in which a debtor may be contacted at all.
|
|
62
|
+
const [open, close] = envelope.collection_hours.split("-") as [string, string];
|
|
63
|
+
if (isOutsideHours(`${close}-${open}`, now, timezone)) {
|
|
64
|
+
return { reason: "outside_hours", detail: `fora do horario de cobranca (${envelope.collection_hours} ${timezone}); agora sao ${localClock(now, timezone)}` };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const documents = new Set(execution.items.map((i) => i.payee));
|
|
68
|
+
if (documents.size !== 1) return { reason: "outside_envelope", detail: "one execution covers one agreement; the items name more than one debtor" };
|
|
69
|
+
const document = execution.items[0]!.payee;
|
|
70
|
+
const agreement = agreementByDocument(document);
|
|
71
|
+
// A debtor the allowlist does not name is refused by the core before this runs; a named one without a book entry is a fixture defect.
|
|
72
|
+
if (!agreement) return { reason: "outside_envelope", detail: "no open agreement for this debtor" };
|
|
73
|
+
|
|
74
|
+
if (execution.items.length > envelope.max_instalments) {
|
|
75
|
+
return { reason: "outside_envelope", detail: `${execution.items.length} parcelas; o envelope permite ate ${envelope.max_instalments}` };
|
|
76
|
+
}
|
|
77
|
+
const floor = floorMinor(agreement, envelope);
|
|
78
|
+
if (execution.total < floor) {
|
|
79
|
+
const pct = Math.round((1 - execution.total / agreement.principal_minor) * 1000) / 10;
|
|
80
|
+
return { reason: "outside_envelope", detail: `total ${execution.total} e ${pct}% abaixo do principal ${agreement.principal_minor}; o desconto maximo e ${envelope.max_discount_pct}% (piso ${floor})` };
|
|
81
|
+
}
|
|
82
|
+
if (execution.total > agreement.principal_minor) {
|
|
83
|
+
return { reason: "outside_envelope", detail: `total ${execution.total} acima do principal ${agreement.principal_minor}; nao se cobra mais do que se deve` };
|
|
84
|
+
}
|
|
85
|
+
const small = execution.items.find((i) => i.amount < envelope.min_instalment_minor);
|
|
86
|
+
if (small) return { reason: "outside_envelope", detail: `parcela de ${small.amount} abaixo do minimo ${envelope.min_instalment_minor}` };
|
|
87
|
+
|
|
88
|
+
const today = localDate(now, timezone);
|
|
89
|
+
const last = addDays(today, envelope.due_date_window_days);
|
|
90
|
+
let previous = "";
|
|
91
|
+
for (const [i, item] of execution.items.entries()) {
|
|
92
|
+
if (!item.due_date) return { reason: "outside_envelope", detail: `parcela ${i + 1} sem vencimento` };
|
|
93
|
+
if (item.due_date < today) return { reason: "outside_envelope", detail: `vencimento ${item.due_date} da parcela ${i + 1} ja passou (hoje e ${today})` };
|
|
94
|
+
if (item.due_date > last) return { reason: "outside_envelope", detail: `vencimento ${item.due_date} da parcela ${i + 1} fora da janela de ${envelope.due_date_window_days} dias (ate ${last})` };
|
|
95
|
+
if (item.due_date < previous) return { reason: "outside_envelope", detail: `parcela ${i + 1} vence antes da anterior` };
|
|
96
|
+
previous = item.due_date;
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function envelopePolicy(envelope: Envelope): PolicyExtension {
|
|
102
|
+
return (execution, ctx) => checkEnvelope(execution, envelope, ctx.now, ctx.guardrails.timezone);
|
|
103
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm start` interactive terminal (you are the payer)
|
|
3
|
+
* `npm start -- --input "oi, recebi a mensagem do acordo 1042"` one turn, no prompt
|
|
4
|
+
* `npm start -- --scenario happy-path` a scenario pack, both modes it declares
|
|
5
|
+
*
|
|
6
|
+
* `--json` puts machine data on stdout (valid JSON, nothing else) and the
|
|
7
|
+
* human messages on stderr.
|
|
8
|
+
*/
|
|
9
|
+
import { stderr, stdout } from "node:process";
|
|
10
|
+
import { relative, resolve } from "node:path";
|
|
11
|
+
import { NotATestKeyError, isTestKey } from "@codespar/agent-core";
|
|
12
|
+
import { closeTerminal, handleExecution, interactive } from "../channels/terminal/index.js";
|
|
13
|
+
import { checkScenario, listScenarios, loadScenario, runScenario, scenariosDir } from "./scenarios.js";
|
|
14
|
+
import { readDotEnv, resolveRailKind, setup, type RailKind } from "./setup.js";
|
|
15
|
+
|
|
16
|
+
interface Args {
|
|
17
|
+
input?: string;
|
|
18
|
+
scenario?: string;
|
|
19
|
+
mode?: "human" | "mandate";
|
|
20
|
+
json: boolean;
|
|
21
|
+
decision?: "approve" | "deny";
|
|
22
|
+
provider?: "anthropic" | "replay";
|
|
23
|
+
transcript?: string;
|
|
24
|
+
rail?: RailKind;
|
|
25
|
+
user: string;
|
|
26
|
+
wait?: number;
|
|
27
|
+
simulatePayer: boolean;
|
|
28
|
+
payer?: "pays" | "expires" | "never";
|
|
29
|
+
help: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseArgs(argv: string[]): Args {
|
|
33
|
+
const args: Args = { json: false, user: "usr_operator", simulatePayer: false, help: false };
|
|
34
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
35
|
+
const a = argv[i]!;
|
|
36
|
+
const next = () => {
|
|
37
|
+
const v = argv[i + 1];
|
|
38
|
+
if (v === undefined) throw new Error(`${a} needs a value`);
|
|
39
|
+
i += 1;
|
|
40
|
+
return v;
|
|
41
|
+
};
|
|
42
|
+
if (a === "--input") args.input = next();
|
|
43
|
+
else if (a === "--scenario") args.scenario = next();
|
|
44
|
+
else if (a === "--mode") {
|
|
45
|
+
const v = next();
|
|
46
|
+
if (v !== "human" && v !== "mandate") throw new Error("--mode must be human or mandate");
|
|
47
|
+
args.mode = v;
|
|
48
|
+
} else if (a === "--json") args.json = true;
|
|
49
|
+
else if (a === "--approve") args.decision = "approve";
|
|
50
|
+
else if (a === "--deny") args.decision = "deny";
|
|
51
|
+
else if (a === "--provider") {
|
|
52
|
+
const v = next();
|
|
53
|
+
if (v !== "anthropic" && v !== "replay") throw new Error("--provider must be anthropic or replay");
|
|
54
|
+
args.provider = v;
|
|
55
|
+
} else if (a === "--transcript") args.transcript = next();
|
|
56
|
+
else if (a === "--rail") {
|
|
57
|
+
const v = next();
|
|
58
|
+
if (v !== "stub" && v !== "api") throw new Error("--rail must be stub or api");
|
|
59
|
+
args.rail = v;
|
|
60
|
+
} else if (a === "--user") args.user = next();
|
|
61
|
+
else if (a === "--wait") {
|
|
62
|
+
const v = Number(next());
|
|
63
|
+
if (!Number.isFinite(v) || v < 0) throw new Error("--wait must be a number of seconds");
|
|
64
|
+
args.wait = v;
|
|
65
|
+
} else if (a === "--simulate-payer") args.simulatePayer = true;
|
|
66
|
+
else if (a === "--payer") {
|
|
67
|
+
const v = next();
|
|
68
|
+
if (v !== "pays" && v !== "expires" && v !== "never") throw new Error("--payer must be pays, expires or never");
|
|
69
|
+
args.payer = v;
|
|
70
|
+
} else if (a === "--help" || a === "-h") args.help = true;
|
|
71
|
+
else throw new Error(`unknown argument ${a}`);
|
|
72
|
+
}
|
|
73
|
+
return args;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const USAGE = `collections-agent
|
|
77
|
+
npm start interactive terminal (you are the payer)
|
|
78
|
+
npm start -- --input "oi, recebi a mensagem do acordo 1042" one turn (add --approve/--deny to decide, --json for machine output)
|
|
79
|
+
npm start -- --scenario <name> run a scenario pack (see scenarios/)
|
|
80
|
+
options: --mode human|mandate --provider anthropic|replay --transcript <file> --rail stub|api --user <id>
|
|
81
|
+
--wait <seconds> --simulate-payer --payer pays|expires|never (stub only) --json`;
|
|
82
|
+
|
|
83
|
+
export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
84
|
+
let args: Args;
|
|
85
|
+
try {
|
|
86
|
+
args = parseArgs(argv);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
stderr.write(`${err instanceof Error ? err.message : String(err)}\n${USAGE}\n`);
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
91
|
+
if (args.help) {
|
|
92
|
+
stderr.write(USAGE + "\n");
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
readDotEnv();
|
|
96
|
+
const say = (line: string) => stderr.write(line + "\n");
|
|
97
|
+
|
|
98
|
+
// Sandbox by construction: a live key dies here, before anything else runs.
|
|
99
|
+
const railKind = resolveRailKind(process.env, args.rail);
|
|
100
|
+
if (railKind === "api" && !isTestKey(process.env["CODESPAR_API_KEY"])) {
|
|
101
|
+
say(new NotATestKeyError().message);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (args.scenario) return runScenarioCommand(args, railKind, say);
|
|
106
|
+
|
|
107
|
+
// One-shot without a model: replay the recorded scenario whose first turn is this input.
|
|
108
|
+
let transcript = args.transcript;
|
|
109
|
+
const provider = args.provider ?? (process.env["ANTHROPIC_API_KEY"] && process.env["ANTHROPIC_API_KEY"] !== "sk-ant-your_key_here" ? "anthropic" : "replay");
|
|
110
|
+
if (provider === "replay" && !transcript && args.input) {
|
|
111
|
+
const match = listScenarios().map(loadScenario).find((s) => s.turns[0].input === args.input);
|
|
112
|
+
if (match) transcript = resolve(scenariosDir(), match.transcript);
|
|
113
|
+
else {
|
|
114
|
+
say(`no ANTHROPIC_API_KEY and no recorded transcript for that input. Set the key, pass --transcript, or use one of: ${listScenarios().map((n) => `"${loadScenario(n).turns[0].input}"`).join(", ")}`);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const s = setup({ mode: args.mode, rail: railKind, provider, transcript, say });
|
|
120
|
+
if (args.payer) s.payer.behave(args.payer);
|
|
121
|
+
const approver = { id: args.user, channel: "terminal" };
|
|
122
|
+
const started = Date.now();
|
|
123
|
+
try {
|
|
124
|
+
const runtime = s.makeRuntime();
|
|
125
|
+
if (!args.input) {
|
|
126
|
+
await interactive({ setup: s, approver, runtime, say, waitSeconds: args.wait, simulatePayer: args.simulatePayer });
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
const loop = s.makeLoop(runtime, (execution) => handleExecution(execution, { setup: s, approver, decision: args.decision ?? "none", say, waitSeconds: args.wait, simulatePayer: args.simulatePayer, tell: args.json ? say : undefined }));
|
|
130
|
+
const result = await loop.turn(args.input);
|
|
131
|
+
const executions = s.engine.list().filter((e) => e.run_id === s.runId);
|
|
132
|
+
const payload = {
|
|
133
|
+
run_id: s.runId,
|
|
134
|
+
agent: `${s.manifest.manifest.name}@${s.manifest.manifest.version}`,
|
|
135
|
+
mode: s.mode,
|
|
136
|
+
rail: s.railKind,
|
|
137
|
+
policy_id: s.mandate.id,
|
|
138
|
+
actor: s.engine.agentActor,
|
|
139
|
+
reply: result.reply,
|
|
140
|
+
tool_calls: result.tool_calls,
|
|
141
|
+
executions: executions.map((e) => ({
|
|
142
|
+
id: e.id,
|
|
143
|
+
state: e.state,
|
|
144
|
+
reason: e.reason ?? null,
|
|
145
|
+
escalation: e.escalation ?? null,
|
|
146
|
+
total_minor: e.total,
|
|
147
|
+
items: e.items.map((i) => ({ debtor: i.beneficiary, amount_minor: i.amount, due_date: i.due_date ?? null })),
|
|
148
|
+
approval_id: e.approval_id ?? null,
|
|
149
|
+
charges: e.outcomes.map((o) => ({ instalment: o.index + 1, charge_id: o.transaction_id ?? null, status: o.status, code: o.code ?? null, payable: o.instrument?.payable ?? null, pix_copy_paste: o.instrument?.pix_copy_paste ?? null })),
|
|
150
|
+
receipt_ids: e.outcomes.filter((o) => o.receipt_id).map((o) => o.receipt_id),
|
|
151
|
+
})),
|
|
152
|
+
receipts: s.bundle.listReceipts().map((f) => relative(process.cwd(), `${s.bundle.dir}/receipts/${f}`)),
|
|
153
|
+
bundle_dir: relative(process.cwd(), s.bundle.dir),
|
|
154
|
+
seconds: Math.round(((Date.now() - started) / 1000) * 10) / 10,
|
|
155
|
+
};
|
|
156
|
+
if (args.json) stdout.write(JSON.stringify(payload) + "\n");
|
|
157
|
+
else stdout.write(result.reply + "\n");
|
|
158
|
+
return executions.some((e) => e.state === "executing") ? 3 : 0;
|
|
159
|
+
} finally {
|
|
160
|
+
closeTerminal();
|
|
161
|
+
s.close();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function runScenarioCommand(args: Args, railKind: RailKind, say: (l: string) => void): Promise<number> {
|
|
166
|
+
const scenario = loadScenario(args.scenario!);
|
|
167
|
+
if (!scenario.rails.includes(railKind)) {
|
|
168
|
+
say(`scenario ${scenario.name} runs on ${scenario.rails.join("/")} only (asked: ${railKind})`);
|
|
169
|
+
return 2;
|
|
170
|
+
}
|
|
171
|
+
const modes = args.mode ? [args.mode] : scenario.modes;
|
|
172
|
+
const results = [];
|
|
173
|
+
for (const mode of modes) {
|
|
174
|
+
say(`== scenario ${scenario.name} — approval: ${mode} — rail: ${railKind}`);
|
|
175
|
+
say(scenario.description);
|
|
176
|
+
const run = await runScenario(scenario, { mode, rail: railKind, say: args.json ? () => undefined : say, tell: args.json ? () => undefined : (l) => stdout.write(l + "\n"), waitSeconds: args.wait });
|
|
177
|
+
const check = checkScenario(scenario, run);
|
|
178
|
+
for (const reply of run.replies) say(`agente: ${reply}`);
|
|
179
|
+
say(check.ok ? `== ok — ${run.cycle_seconds}s — bundle em ${relative(process.cwd(), run.bundle_dir)}` : `== FAIL: ${check.failures.join("; ")}`);
|
|
180
|
+
results.push({ mode, ok: check.ok, failures: check.failures, run });
|
|
181
|
+
}
|
|
182
|
+
if (args.json) stdout.write(JSON.stringify({ scenario: scenario.name, rail: railKind, results }) + "\n");
|
|
183
|
+
return results.every((r) => r.ok) ? 0 : 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
187
|
+
main()
|
|
188
|
+
.then((code) => process.exit(code))
|
|
189
|
+
.catch((err) => {
|
|
190
|
+
stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
});
|
|
193
|
+
}
|
package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module `bolepix-receivables`: the tool handlers the model can reach.
|
|
3
|
+
* `codespar_charge` hands the proposal to the engine and gets an execution in
|
|
4
|
+
* `drafted` (then whatever the core decided); it cannot issue anything.
|
|
5
|
+
* `list_agreements` is a read. The real call (`POST /v1/charges`, one
|
|
6
|
+
* cobranca com vencimento per instalment, `idempotency_key` per attempt) is
|
|
7
|
+
* built by the rail from the execution the core approved.
|
|
8
|
+
*/
|
|
9
|
+
import type { ToolHandler } from "@codespar/agent-core";
|
|
10
|
+
import { AGREEMENTS, agreementByAlias, formatBRL, formatDate } from "../agreements.js";
|
|
11
|
+
import type { Envelope } from "../envelope.js";
|
|
12
|
+
|
|
13
|
+
interface ChargeInput {
|
|
14
|
+
action?: unknown;
|
|
15
|
+
agreement?: unknown;
|
|
16
|
+
instalments?: unknown;
|
|
17
|
+
total_minor?: unknown;
|
|
18
|
+
execution_id?: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function makeHandlers(envelope: Envelope): Record<string, ToolHandler> {
|
|
22
|
+
const listAgreements: ToolHandler = async (_input, ctx) => {
|
|
23
|
+
const settled = ctx.engine.list({ state: "settled" });
|
|
24
|
+
const open = ctx.engine.list({ state: "executing" });
|
|
25
|
+
const settledAliases = new Set(settled.flatMap((e) => e.items.map((i) => i.alias)).filter(Boolean));
|
|
26
|
+
const issuedAliases = new Set(open.flatMap((e) => e.items.map((i) => i.alias)).filter(Boolean));
|
|
27
|
+
return {
|
|
28
|
+
agreements: AGREEMENTS.map((a) => ({
|
|
29
|
+
alias: a.alias,
|
|
30
|
+
debtor_first_name: a.first_name,
|
|
31
|
+
principal_minor: a.principal_minor,
|
|
32
|
+
principal: formatBRL(a.principal_minor),
|
|
33
|
+
origin: a.origin,
|
|
34
|
+
opened_at: a.opened_at,
|
|
35
|
+
status: settledAliases.has(a.alias) ? "quitado" : issuedAliases.has(a.alias) ? "cobranca emitida, aguardando pagamento" : "em aberto",
|
|
36
|
+
})),
|
|
37
|
+
envelope: {
|
|
38
|
+
max_discount_pct: envelope.max_discount_pct,
|
|
39
|
+
max_instalments: envelope.max_instalments,
|
|
40
|
+
due_date_window_days: envelope.due_date_window_days,
|
|
41
|
+
min_instalment_minor: envelope.min_instalment_minor,
|
|
42
|
+
collection_hours: envelope.collection_hours,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const codesparCharge: ToolHandler = async (raw, ctx) => {
|
|
48
|
+
const input = raw as ChargeInput;
|
|
49
|
+
const action = input.action ?? "create";
|
|
50
|
+
if (action === "status") {
|
|
51
|
+
if (typeof input.execution_id !== "string") throw new Error("codespar_charge: execution_id is required for action=status");
|
|
52
|
+
const execution = ctx.engine.get(input.execution_id);
|
|
53
|
+
if (!execution) return { found: false, message: "no execution with that id in this run" };
|
|
54
|
+
return { found: true, ...describe(execution) };
|
|
55
|
+
}
|
|
56
|
+
if (action !== "create") throw new Error(`codespar_charge: unsupported action ${String(action)}; this agent only creates and reads receivables`);
|
|
57
|
+
if (typeof input.agreement !== "string" || !input.agreement.trim()) throw new Error("codespar_charge: agreement must be an agreement alias from list_agreements");
|
|
58
|
+
if (!Array.isArray(input.instalments) || input.instalments.length === 0) throw new Error("codespar_charge: instalments must be a non-empty array");
|
|
59
|
+
const alias = input.agreement.trim().toLowerCase();
|
|
60
|
+
const agreement = agreementByAlias(alias);
|
|
61
|
+
const count = input.instalments.length;
|
|
62
|
+
const items = input.instalments.map((entry, i) => {
|
|
63
|
+
const it = entry as { amount_minor?: unknown; due_date?: unknown };
|
|
64
|
+
if (typeof it.amount_minor !== "number" || !Number.isInteger(it.amount_minor) || it.amount_minor <= 0) throw new Error(`codespar_charge: instalments[${i}].amount_minor must be a positive integer`);
|
|
65
|
+
if (typeof it.due_date !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(it.due_date)) throw new Error(`codespar_charge: instalments[${i}].due_date must be YYYY-MM-DD`);
|
|
66
|
+
return {
|
|
67
|
+
payee: alias,
|
|
68
|
+
amount: it.amount_minor,
|
|
69
|
+
due_date: it.due_date,
|
|
70
|
+
description: `${agreement ? `Acordo ${agreement.alias.replace("acordo-", "#")}` : `Acordo ${alias}`} - parcela ${i + 1}/${count}`,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
const claimed = typeof input.total_minor === "number" ? { claimed_total: input.total_minor } : {};
|
|
74
|
+
|
|
75
|
+
const draft = await ctx.engine.draft({ items, ...claimed });
|
|
76
|
+
if (!draft.ok) return { status: "refused", reason: draft.reason, message: draft.message, issued: false, paid: false };
|
|
77
|
+
const execution = await ctx.onExecution(draft.execution);
|
|
78
|
+
return describe(execution);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return { codespar_charge: codesparCharge, list_agreements: listAgreements };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function describe(execution: import("@codespar/agent-core").Execution) {
|
|
85
|
+
const charges = execution.outcomes.map((o) => ({
|
|
86
|
+
instalment: o.index + 1,
|
|
87
|
+
charge_id: o.transaction_id ?? null,
|
|
88
|
+
status: o.status,
|
|
89
|
+
...(o.instrument ? { payable: o.instrument.payable, pix_copy_paste: o.instrument.pix_copy_paste, boleto_bank_line: o.instrument.boleto_bank_line, due_date: o.instrument.due_date } : {}),
|
|
90
|
+
...(o.code ? { code: o.code } : {}),
|
|
91
|
+
}));
|
|
92
|
+
return {
|
|
93
|
+
status: execution.state,
|
|
94
|
+
execution_id: execution.id,
|
|
95
|
+
total_minor: execution.total,
|
|
96
|
+
total: formatBRL(execution.total),
|
|
97
|
+
instalments: execution.items.map((i, n) => ({ instalment: n + 1, amount: formatBRL(i.amount), due_date: i.due_date ? formatDate(i.due_date) : null })),
|
|
98
|
+
...(execution.reason ? { reason: execution.reason, detail: execution.detail } : {}),
|
|
99
|
+
...(execution.escalation ? { escalated_by: execution.escalation.trigger } : {}),
|
|
100
|
+
charges,
|
|
101
|
+
issued: charges.length > 0 && charges.every((c) => c.status !== "failed"),
|
|
102
|
+
paid: execution.state === "settled",
|
|
103
|
+
expired: execution.state === "failed" && execution.reason === "charge_expired",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the cycle closes without a webhook: the kit LOOKS at the receivable
|
|
3
|
+
* (`GET /v1/charges/{id}` through the rail's `lookup`, the stub's fixture
|
|
4
|
+
* offline) until the payer acted or the wait ran out. Each look is a
|
|
5
|
+
* `reconcile`, which is read-only on the rail and never re-issues. The
|
|
6
|
+
* instrument (QR, copy-and-paste, bank line) is presented the first time a
|
|
7
|
+
* look finds it payable; the debtor is told the outcome once, whatever the
|
|
8
|
+
* number of looks or events that carried it.
|
|
9
|
+
*/
|
|
10
|
+
import type { ChargeInstrument, Execution, ExecutionEngine } from "@codespar/agent-core";
|
|
11
|
+
import type { SandboxPayer } from "./setup.js";
|
|
12
|
+
|
|
13
|
+
export interface PollHooks {
|
|
14
|
+
/** A receivable became payable: show the payer how to pay. Called once per instalment. */
|
|
15
|
+
onInstrument?: (execution: Execution, instalment: number, chargeId: string, instrument: ChargeInstrument) => void;
|
|
16
|
+
/** The execution reached a terminal state. Called once. */
|
|
17
|
+
onClosed?: (execution: Execution) => void;
|
|
18
|
+
/** Called before each look after the first. */
|
|
19
|
+
onWait?: (execution: Execution, round: number) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PollOptions extends PollHooks {
|
|
23
|
+
intervalMs: number;
|
|
24
|
+
/** Wall-clock budget for the wait. */
|
|
25
|
+
timeoutMs: number;
|
|
26
|
+
/** Cap on looks, for a zero-interval (stub) loop: the fixture payer acts within two looks; a third proves nothing changes. */
|
|
27
|
+
maxRounds?: number;
|
|
28
|
+
/** When set, the payer plays as soon as every instalment is payable (a scenario, `--simulate-payer`). */
|
|
29
|
+
payer?: SandboxPayer | undefined;
|
|
30
|
+
clock?: () => Date;
|
|
31
|
+
sleep?: (ms: number) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PollResult {
|
|
35
|
+
execution: Execution;
|
|
36
|
+
rounds: number;
|
|
37
|
+
/** Seconds from the first look to the terminal state, or to the end of the wait. */
|
|
38
|
+
seconds: number;
|
|
39
|
+
timed_out: boolean;
|
|
40
|
+
payer_calls: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const defaultSleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Presents each instalment once, records `message.debtor` once per outcome,
|
|
47
|
+
* and returns when the execution is terminal or the wait is over. Safe to
|
|
48
|
+
* call again on the same execution (a restart): what was already shown is
|
|
49
|
+
* remembered in the state.db cursor table, not in memory.
|
|
50
|
+
*/
|
|
51
|
+
export async function pollUntilClosed(engine: ExecutionEngine, executionId: string, options: PollOptions): Promise<PollResult> {
|
|
52
|
+
const clock = options.clock ?? (() => new Date());
|
|
53
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
54
|
+
const startedWall = Date.now();
|
|
55
|
+
const started = clock().getTime();
|
|
56
|
+
const maxRounds = options.maxRounds ?? (options.intervalMs === 0 ? 3 : Number.POSITIVE_INFINITY);
|
|
57
|
+
const payerCalls: string[] = [];
|
|
58
|
+
let execution = engine.get(executionId);
|
|
59
|
+
if (!execution) throw new Error(`unknown execution ${executionId}`);
|
|
60
|
+
let rounds = 0;
|
|
61
|
+
let paidRequested = false;
|
|
62
|
+
|
|
63
|
+
const present = (e: Execution) => {
|
|
64
|
+
for (const outcome of e.outcomes) {
|
|
65
|
+
if (outcome.status !== "accepted" || !outcome.instrument?.payable || !outcome.transaction_id) continue;
|
|
66
|
+
if (!engine.markShown(e.id, outcome.attempt_id)) continue;
|
|
67
|
+
options.onInstrument?.(e, outcome.index + 1, outcome.transaction_id, outcome.instrument);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
present(execution);
|
|
72
|
+
while (execution.state === "executing") {
|
|
73
|
+
if (rounds > 0) {
|
|
74
|
+
// A zero-interval loop (the stub) is bounded by looks; a real one by the wall clock.
|
|
75
|
+
if (rounds >= maxRounds || (options.intervalMs > 0 && Date.now() - startedWall >= options.timeoutMs)) break;
|
|
76
|
+
options.onWait?.(execution, rounds);
|
|
77
|
+
if (options.intervalMs > 0) await sleep(options.intervalMs);
|
|
78
|
+
}
|
|
79
|
+
rounds += 1;
|
|
80
|
+
execution = await engine.reconcile(execution.id);
|
|
81
|
+
present(execution);
|
|
82
|
+
const payable = execution.outcomes.filter((o) => o.status === "accepted" && o.instrument?.payable && o.transaction_id);
|
|
83
|
+
if (options.payer && !paidRequested && execution.state === "executing" && payable.length > 0 && payable.length === execution.outcomes.filter((o) => o.status === "accepted").length) {
|
|
84
|
+
paidRequested = true;
|
|
85
|
+
for (const outcome of payable) {
|
|
86
|
+
const result = await options.payer.pay(outcome.transaction_id!, outcome.attempt_id);
|
|
87
|
+
payerCalls.push(result.detail);
|
|
88
|
+
engine.note("sandbox_payer", execution.id, { charge_id: outcome.transaction_id, ok: result.ok, detail: result.detail });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (execution.state !== "executing") options.onClosed?.(execution);
|
|
93
|
+
return { execution, rounds, seconds: Math.round(((clock().getTime() - started) / 1000) * 10) / 10, timed_out: execution.state === "executing", payer_calls: payerCalls };
|
|
94
|
+
}
|