@argentic-ai/client 0.0.0-stage → 0.1.14

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/dist/cli.js ADDED
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ // `npx @argentic-ai/client demo`: kill an agent mid-run, run it again, and watch it skip what already finished.
3
+ // Uses only the published client. State lives in ./.argentic-demo/ so a rerun from the same folder picks up the same run.
4
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { Argentic, freeModel, hintFor, runDurable } from "./argentic.js";
7
+ const color = process.stdout.isTTY && !process.env.NO_COLOR;
8
+ const paint = (code) => (s) => (color ? `\x1b[${code}m${s}\x1b[0m` : s);
9
+ const dim = paint('2');
10
+ const bold = paint('1');
11
+ const green = paint('32');
12
+ const yellow = paint('33');
13
+ const out = (s = '') => process.stdout.write(s + '\n');
14
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
15
+ const system = 'Tool calls are requests: their outcomes arrive in the next message. ' +
16
+ 'Call one tool per turn. When everything is done, reply with a one-line summary and no tool call.';
17
+ const task = 'Order 1182 (ticket T-77, customer dana@example.com) arrived broken. Refund the $42 order, ' +
18
+ 'email the customer that the refund is on its way, then close the ticket. Do one step at a time.';
19
+ const usage = `Usage: npx @argentic-ai/client demo [--fresh] [--kill-after N]
20
+
21
+ --fresh start a new run (forget the one in ./.argentic-demo/)
22
+ --kill-after N kill the process yourself after N actions, instead of pressing Ctrl-C
23
+
24
+ Needs ARGENTIC_API_KEY (from https://getargentic.com/platform) and MODEL_KEY
25
+ (your model provider's key; the default model is a free NVIDIA one, key from https://build.nvidia.com).`;
26
+ /** The side effects land in outbox.jsonl, keyed by the action's stable key, so a repeat is visible and harmless. */
27
+ function makeTools(outbox, workMs) {
28
+ const sent = (key) => existsSync(outbox) && readFileSync(outbox, 'utf8').split('\n').some((l) => l && JSON.parse(l).key === key);
29
+ const act = async (key, seq, tool, what, data, result) => {
30
+ process.stdout.write(` ${bold('▸')} #${seq} ${tool.padEnd(14)} ${what} ${dim('…')}`);
31
+ await sleep(workMs);
32
+ if (sent(key)) {
33
+ out(` ${yellow('already in the outbox under this key, not sent again')}`);
34
+ }
35
+ else {
36
+ appendFileSync(outbox, JSON.stringify({ key, tool, ...data, at: new Date().toISOString() }) + '\n');
37
+ out(` ${green('done')}`);
38
+ }
39
+ return result;
40
+ };
41
+ const str = { type: 'string' };
42
+ return [
43
+ {
44
+ name: 'refund_order',
45
+ description: 'Refund an order.',
46
+ parameters: { type: 'object', properties: { order_id: str, amount_usd: { type: 'number' } }, required: ['order_id', 'amount_usd'] },
47
+ execute: (a, { idempotencyKey, seq }) => act(idempotencyKey, seq, 'refund_order', `$${a.amount_usd} on order ${a.order_id}`, a, { refunded: true, order_id: a.order_id, amount_usd: a.amount_usd }),
48
+ },
49
+ {
50
+ name: 'send_email',
51
+ description: 'Send an email to a customer.',
52
+ parameters: { type: 'object', properties: { to: str, subject: str, body: str }, required: ['to', 'subject', 'body'] },
53
+ execute: (a, { idempotencyKey, seq }) => act(idempotencyKey, seq, 'send_email', `to ${a.to}`, a, { sent: true, to: a.to }),
54
+ },
55
+ {
56
+ name: 'close_ticket',
57
+ description: 'Close a support ticket with a note.',
58
+ parameters: { type: 'object', properties: { ticket_id: str, note: str }, required: ['ticket_id', 'note'] },
59
+ execute: (a, { idempotencyKey, seq }) => act(idempotencyKey, seq, 'close_ticket', `ticket ${a.ticket_id}`, a, { closed: true, ticket_id: a.ticket_id }),
60
+ },
61
+ ];
62
+ }
63
+ function modelFromEnv() {
64
+ const e = process.env;
65
+ if (!e.ARGENTIC_MODEL_ID)
66
+ return freeModel;
67
+ return {
68
+ provider: e.ARGENTIC_MODEL_PROVIDER ?? freeModel.provider,
69
+ id: e.ARGENTIC_MODEL_ID,
70
+ prices: { in: Number(e.ARGENTIC_PRICE_IN ?? 0), out: Number(e.ARGENTIC_PRICE_OUT ?? 0) },
71
+ };
72
+ }
73
+ async function demo(argv) {
74
+ const apiKey = process.env.ARGENTIC_API_KEY;
75
+ const modelKey = process.env.MODEL_KEY;
76
+ if (!apiKey || !modelKey) {
77
+ console.error(`Set ARGENTIC_API_KEY and MODEL_KEY first.\n\n${usage}`);
78
+ process.exit(1);
79
+ }
80
+ const at = argv.indexOf('--kill-after');
81
+ const killAfter = at >= 0 ? Number(argv[at + 1]) || undefined : undefined;
82
+ const dir = '.argentic-demo';
83
+ const statePath = join(dir, 'run.json');
84
+ const ledgerPath = join(dir, 'ledger.json');
85
+ const outbox = join(dir, 'outbox.jsonl');
86
+ if (argv.includes('--fresh'))
87
+ rmSync(dir, { recursive: true, force: true });
88
+ mkdirSync(dir, { recursive: true });
89
+ const resuming = existsSync(statePath);
90
+ if (!resuming) {
91
+ const id = Math.random().toString(36).slice(2, 8);
92
+ writeFileSync(statePath, JSON.stringify({ agent: `kill-demo-${id}`, runId: `run-${id}` }));
93
+ }
94
+ const { agent, runId } = JSON.parse(readFileSync(statePath, 'utf8'));
95
+ const finishedBefore = existsSync(ledgerPath) ? Object.keys(JSON.parse(readFileSync(ledgerPath, 'utf8')).entries ?? {}).length : 0;
96
+ out(bold('Argentic: kill it mid-run'));
97
+ out(dim(`agent ${agent}, run ${runId}, state in ./${dir}/`));
98
+ out();
99
+ if (!resuming) {
100
+ out('A support agent works one ticket in three actions: refund the order, email the customer, close the ticket.');
101
+ out(`Press ${bold('Ctrl-C')} while it works, then run the same command again.`);
102
+ }
103
+ else {
104
+ out(`Picking up ${runId}: ${finishedBefore} action${finishedBefore === 1 ? '' : 's'} finished before it stopped.`);
105
+ out('Same run, same keys. Finished actions are skipped, the rest carry on.');
106
+ }
107
+ out();
108
+ process.on('SIGINT', () => {
109
+ out('\n');
110
+ out(yellow('Killed mid-run.') + ' Run the same command again and it picks up where it stopped.');
111
+ process.exit(130);
112
+ });
113
+ const client = new Argentic({ apiKey, modelKey });
114
+ let ran = 0;
115
+ let hinted = resuming || killAfter !== undefined;
116
+ const res = await runDurable({
117
+ client,
118
+ agent,
119
+ runId,
120
+ task,
121
+ system,
122
+ tools: makeTools(outbox, Number(process.env.ARGENTIC_DEMO_WORK_MS ?? 1500)),
123
+ ledgerPath,
124
+ model: modelFromEnv(),
125
+ maxSteps: 10,
126
+ crashAfterActions: killAfter,
127
+ log: () => { },
128
+ onWakeStart: ({ step }) => {
129
+ if (step === 0 && !resuming)
130
+ out(dim(' The first answer can take 15 seconds and nothing runs before it, so wait for the first action before pressing Ctrl-C.'));
131
+ process.stdout.write(dim(` model turn ${step + 1}: asking the model… `));
132
+ },
133
+ onWake: ({ replayed }) => {
134
+ if (replayed)
135
+ out(dim('answer already in the journal, sent back without calling the model again'));
136
+ else
137
+ out(dim('answered'));
138
+ },
139
+ onSkip: ({ tool, seq }) => out(` ${dim('↷')} #${seq} ${tool.padEnd(14)} ${dim('finished on an earlier run, skipped')}`),
140
+ onAction: ({ executed }) => {
141
+ ran = executed;
142
+ if (killAfter && executed >= killAfter)
143
+ out(yellow(`\nKilling the process now (--kill-after ${killAfter}). Run the same command without it.`));
144
+ else if (!hinted) {
145
+ hinted = true;
146
+ out(yellow(' Now is a good time to press Ctrl-C.'));
147
+ }
148
+ },
149
+ });
150
+ out();
151
+ out(`${bold('Reply:')} ${res.reply || dim('(none)')}`);
152
+ out(dim(`status ${res.status}, ${res.steps} model turns, ${ran} action${ran === 1 ? '' : 's'} run in this process, spent $${res.spentUsd.toFixed(4)}`));
153
+ const counts = { refund_order: 0, send_email: 0, close_ticket: 0 };
154
+ if (existsSync(outbox))
155
+ for (const l of readFileSync(outbox, 'utf8').split('\n'))
156
+ if (l)
157
+ counts[JSON.parse(l).tool] = (counts[JSON.parse(l).tool] ?? 0) + 1;
158
+ out();
159
+ out(bold('What happened, from the outbox') + dim(` (./${outbox})`));
160
+ for (const [tool, n] of Object.entries(counts))
161
+ out(` ${tool.padEnd(14)} ${n === 1 ? green('1 time') : yellow(`${n} times`)}`);
162
+ out();
163
+ out(bold('Proof of what ran') + dim(' (re-renders each prompt from the journal and compares it with what the model was sent; free)'));
164
+ let clean = true;
165
+ for (const wake of res.wakes) {
166
+ const r = await client.replay(agent, wake);
167
+ const ok = r.mismatched === 0 && r.unverifiable === 0;
168
+ clean &&= ok;
169
+ out(` ${wake} ${ok ? green('all match') : yellow(`${r.mismatched} differ, ${r.unverifiable} not checkable`)}`);
170
+ }
171
+ if (res.status !== 'done')
172
+ out(yellow(`\nThe run stopped with status ${res.status}. Run the same command again to carry on.`));
173
+ const dash = (process.env.ARGENTIC_DASHBOARD_URL ?? 'https://getargentic.com').replace(/\/+$/, '');
174
+ out();
175
+ out(`See it in the dashboard: ${dash}/platform/ ${dim('->')} Runs ${dim('->')} ${agent}`);
176
+ out(dim('Open the oldest run (the one you killed) and press Check under Proof of what ran.'));
177
+ if (resuming && res.status === 'done') {
178
+ out();
179
+ out(`${bold('Worth showing someone?')} On that run page, press Share proof for a read-only link.`);
180
+ out(dim('It shows what ran and each action’s key. Messages and tool data stay private, and you can stop sharing any time.'));
181
+ }
182
+ out(dim('Start over with --fresh.'));
183
+ if (!clean)
184
+ process.exitCode = 1;
185
+ }
186
+ const [cmd, ...rest] = process.argv.slice(2);
187
+ if (cmd !== 'demo') {
188
+ out(usage);
189
+ process.exit(cmd === undefined || cmd === '--help' || cmd === '-h' ? 0 : 1);
190
+ }
191
+ await demo(rest).catch((e) => {
192
+ const hint = hintFor(e);
193
+ if (!hint)
194
+ throw e;
195
+ console.error(`\n${e instanceof Error ? e.message : String(e)}\n${hint}`);
196
+ process.exit(1);
197
+ });
@@ -0,0 +1,14 @@
1
+ import { type DurableTool } from './argentic.ts';
2
+ interface MastraToolLike {
3
+ description?: string;
4
+ inputSchema?: unknown;
5
+ execute?: (input: any, context: any) => Promise<unknown>;
6
+ }
7
+ /**
8
+ * The tool name is the key, as when you pass `tools` to an Agent.
9
+ * `oncePer` maps a tool name to its `once_per` template, such as `{ refund_order: 'refund:{order_id}' }`.
10
+ */
11
+ export declare function fromMastraTools(tools: Record<string, MastraToolLike>, opts?: {
12
+ oncePer?: Record<string, string>;
13
+ }): DurableTool[];
14
+ export {};
package/dist/mastra.js ADDED
@@ -0,0 +1,29 @@
1
+ // Mastra on-ramp: your createTool() tools become Argentic tools.
2
+ // Only the tools carry over. Argentic runs the loop, not Agent.generate or Agent.stream.
3
+ import { withOncePer } from "./argentic.js";
4
+ /**
5
+ * The tool name is the key, as when you pass `tools` to an Agent.
6
+ * `oncePer` maps a tool name to its `once_per` template, such as `{ refund_order: 'refund:{order_id}' }`.
7
+ */
8
+ export function fromMastraTools(tools, opts = {}) {
9
+ return withOncePer(Object.entries(tools).map(([name, t]) => {
10
+ const std = t.inputSchema?.['~standard'];
11
+ const parameters = std?.jsonSchema?.input({ target: 'draft-07' }) ?? { type: 'object', properties: {} };
12
+ return {
13
+ name,
14
+ description: t.description ?? '',
15
+ parameters,
16
+ execute: async (args, ctx) => {
17
+ if (!t.execute)
18
+ return { error: `tool ${name} has no execute function` };
19
+ if (std) {
20
+ const checked = (await std.validate(args));
21
+ if (checked.issues?.length)
22
+ return { error: `invalid input: ${checked.issues.map((i) => i.message).join('; ')}` };
23
+ }
24
+ // The action's idempotency key is the tool call id, as agent.toolCallId is in a Mastra run.
25
+ return t.execute(args, { agent: { toolCallId: ctx.idempotencyKey, messages: [] } });
26
+ },
27
+ };
28
+ }), opts.oncePer);
29
+ }
@@ -0,0 +1,75 @@
1
+ import { type DurableTool } from './argentic.ts';
2
+ /** What to do with a Slack, GitHub or SMTP action that may or may not have landed and cannot be checked. */
3
+ export type Ambiguous = 'hold' | 'retry';
4
+ export interface StripeOptions {
5
+ secretKey: string;
6
+ baseUrl?: string;
7
+ /** Refuse any refund or charge above this many minor units (cents), whatever the model asks. */
8
+ maxAmount?: number;
9
+ }
10
+ export interface ResendOptions {
11
+ apiKey: string;
12
+ /** The From address. The model never picks it. */
13
+ from: string;
14
+ baseUrl?: string;
15
+ }
16
+ export interface SmtpOptions {
17
+ host: string;
18
+ port?: number;
19
+ /** Implicit TLS (port 465). Otherwise STARTTLS is used when the server offers it. */
20
+ secure?: boolean;
21
+ user?: string;
22
+ pass?: string;
23
+ from: string;
24
+ /** Default 'hold': a send cut off after the message was handed over is not sent again. */
25
+ onAmbiguous?: Ambiguous;
26
+ /** Skip certificate checks. For a local test server only. */
27
+ insecureTls?: boolean;
28
+ }
29
+ export interface SlackOptions {
30
+ token: string;
31
+ baseUrl?: string;
32
+ /** Only these channel ids may be posted to. */
33
+ channels?: string[];
34
+ /** Default 'hold'. Looking a message up needs the channel's history scope; without it the tool cannot check. */
35
+ onAmbiguous?: Ambiguous;
36
+ }
37
+ export interface GithubOptions {
38
+ token: string;
39
+ baseUrl?: string;
40
+ /** Only these `owner/repo` names may be written to. */
41
+ repos?: string[];
42
+ onAmbiguous?: Ambiguous;
43
+ }
44
+ export interface HttpOptions {
45
+ /** Hosts the model may POST to. Nothing else is called. */
46
+ allowHosts: string[];
47
+ /** Fixed headers, such as Authorization. The model never sees or sets them. */
48
+ headers?: Record<string, string>;
49
+ /** Allow http:// to an allowed host. Off by default. */
50
+ allowHttp?: boolean;
51
+ }
52
+ export interface ToolPackOptions {
53
+ stripe?: StripeOptions;
54
+ resend?: ResendOptions;
55
+ smtp?: SmtpOptions;
56
+ slack?: SlackOptions;
57
+ github?: GithubOptions;
58
+ http?: HttpOptions;
59
+ /** Where intents are kept so a restart knows what was started. Default `.argentic-intents.json`. */
60
+ statePath?: string;
61
+ /** Tries per action inside one process before the run stops with RetryLater. Default 4. */
62
+ tries?: number;
63
+ /** Waits between tries, in ms. Default 500, 1500, 4000. */
64
+ backoffMs?: number[];
65
+ fetch?: typeof fetch;
66
+ sleep?: (ms: number) => Promise<void>;
67
+ }
68
+ /** A key a provider accepts (Stripe allows 255 characters, Resend 256). */
69
+ export declare function providerKey(key: string): string;
70
+ /**
71
+ * Tools for runDurable. Pass only the services you use; each tool is built from your options and your
72
+ * secrets, never from anything the model sends. With Resend and SMTP both set, Resend is `send_email`
73
+ * and SMTP is `send_email_smtp`.
74
+ */
75
+ export declare function toolPack(opts: ToolPackOptions): DurableTool[];