@fastagent-sh/fastagent 0.26.0 → 0.26.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/dist/harnesses/models/login.js +6 -6
- package/dist/harnesses/pi-durable/agent.js +2 -0
- package/dist/harnesses/pi-durable/ids.js +2 -0
- package/dist/harnesses/pi-durable/parked.d.ts +18 -0
- package/dist/harnesses/pi-durable/parked.js +27 -0
- package/dist/harnesses/pi-durable/release.d.ts +3 -2
- package/dist/harnesses/pi-durable/storage.js +4 -2
- package/dist/http/contract.d.ts +3 -3
- package/dist/schedule/cron.d.ts +4 -2
- package/dist/schedule/cron.js +4 -2
- package/dist/schedule/eventbridge-cron.d.ts +7 -3
- package/dist/schedule/eventbridge-cron.js +9 -8
- package/package.json +16 -16
|
@@ -239,11 +239,11 @@ export const PI_DEFAULT_MODELS = {
|
|
|
239
239
|
azure: "gpt-5.4",
|
|
240
240
|
"openai-codex": "gpt-6.1-sol",
|
|
241
241
|
radius: "balanced",
|
|
242
|
-
nvidia: "nvidia/nemotron-3-
|
|
242
|
+
nvidia: "nvidia/nemotron-3.5-super-vl-120b-a12b",
|
|
243
243
|
deepseek: "deepseek-v4-pro",
|
|
244
244
|
google: "gemini-3.1-pro-preview",
|
|
245
245
|
"google-vertex": "gemini-3.1-pro-preview",
|
|
246
|
-
"github-copilot": "gpt-
|
|
246
|
+
"github-copilot": "gpt-6.1-sol",
|
|
247
247
|
openrouter: "moonshotai/kimi-k2.6",
|
|
248
248
|
"vercel-ai-gateway": "zai/glm-5.1",
|
|
249
249
|
xai: "grok-4.7",
|
|
@@ -269,10 +269,10 @@ export const PI_DEFAULT_MODELS = {
|
|
|
269
269
|
"qwen-token-plan": "qwen3.7-max",
|
|
270
270
|
"qwen-token-plan-cn": "qwen3.7-max",
|
|
271
271
|
"qwen-token-plan-individual": "qwen3.8-max",
|
|
272
|
-
xiaomi: "mimo-v2.
|
|
273
|
-
"xiaomi-token-plan-cn": "mimo-v2.
|
|
274
|
-
"xiaomi-token-plan-ams": "mimo-v2.
|
|
275
|
-
"xiaomi-token-plan-sgp": "mimo-v2.
|
|
272
|
+
xiaomi: "mimo-v2.6-pro",
|
|
273
|
+
"xiaomi-token-plan-cn": "mimo-v2.6-pro",
|
|
274
|
+
"xiaomi-token-plan-ams": "mimo-v2.6-pro",
|
|
275
|
+
"xiaomi-token-plan-sgp": "mimo-v2.6-pro",
|
|
276
276
|
};
|
|
277
277
|
/** One minimal request with the key under test. */
|
|
278
278
|
async function verifyApiKey(models, providerId, verifyWith, notify, signal) {
|
|
@@ -22,6 +22,7 @@ import { admitSettings } from "./defaults.js";
|
|
|
22
22
|
import { USER_ENTRY, imageAt, isPublished, parseImageRef } from "./entries.js";
|
|
23
23
|
import { sessionEvents } from "./events.js";
|
|
24
24
|
import { storageIds } from "./ids.js";
|
|
25
|
+
import { giveUpOnParked } from "./parked.js";
|
|
25
26
|
import { isInput, queuedInputs, requestIdFor, usageOfState } from "./messages.js";
|
|
26
27
|
import { durableReads } from "./reads.js";
|
|
27
28
|
import { releaseWaitingQueues } from "./release.js";
|
|
@@ -42,6 +43,7 @@ export async function openDurableAgent(options) {
|
|
|
42
43
|
try {
|
|
43
44
|
const ids = await storageIds(harness, context);
|
|
44
45
|
const sessions = await Effect.runPromise(Scope.provide(trackSessions(harness, options.storage, context), lifetime));
|
|
46
|
+
await giveUpOnParked(harness, context);
|
|
45
47
|
// Before `resume()`, which runs an earlier process's work. An abort enables the scheduler too, but commits first, so
|
|
46
48
|
// the aborted work does not run (measured).
|
|
47
49
|
const unsettled = (await harness.inspect(context)).submissions.filter(isInput);
|
|
@@ -11,6 +11,8 @@ const StorageDoc = defineDoc({
|
|
|
11
11
|
version: 1,
|
|
12
12
|
scope: "session",
|
|
13
13
|
initial: () => ({ incarnation: "" }),
|
|
14
|
+
// Written once, when the storage is first opened: no deltas pile up to bound.
|
|
15
|
+
checkpointWhen: () => false,
|
|
14
16
|
});
|
|
15
17
|
export async function storageIds(harness, context) {
|
|
16
18
|
const incarnation = await harness.commit(async (tx) => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What becomes of a task pi-durable's crash-loop guard parked (`settings.restartLimit`: cut off by a crash that many
|
|
3
|
+
* times in a row, with no progress between). Left parked, the message it was answering stays `running` for ever with
|
|
4
|
+
* nothing to say why (SPEC.md §5.4 ends a message that cannot be answered `failed`). Parked is a state the task keeps,
|
|
5
|
+
* which `inspect()` shows as blocked for `restart_limit`: it is read from there, and not from the one `task_parked`
|
|
6
|
+
* report an open makes, so a process that dies half way, or a step that fails, is finished by the next open.
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from "@earendil-works/chord/context";
|
|
9
|
+
import { type Harness } from "@earendil-works/pi-durable";
|
|
10
|
+
/**
|
|
11
|
+
* Settle the messages each parked task was answering `unanswered` for {@link RESTART_LIMIT_REASON}, and abort the task,
|
|
12
|
+
* which frees its session. The settlement comes first: an abort alone settles them `aborted`, which reads as a
|
|
13
|
+
* cancellation nobody asked for, and the first settlement of a submission stands (measured). Both steps can be run
|
|
14
|
+
* again over their own result: the run stays until the abort's handler ends it, so a task left half way is found
|
|
15
|
+
* parked, and finished, by the next open. A parked task that is not its conversation's run (a tool's, a compaction's)
|
|
16
|
+
* is only aborted: the run goes on without it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function giveUpOnParked(harness: Harness, context: Context): Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { LiveDoc } from "@earendil-works/pi-durable";
|
|
2
|
+
import { log } from "../../log.js";
|
|
3
|
+
const RESTART_LIMIT_REASON = "restart_limit";
|
|
4
|
+
const DETAIL = "The process was cut off repeatedly while answering this message, so it was given up on";
|
|
5
|
+
/**
|
|
6
|
+
* Settle the messages each parked task was answering `unanswered` for {@link RESTART_LIMIT_REASON}, and abort the task,
|
|
7
|
+
* which frees its session. The settlement comes first: an abort alone settles them `aborted`, which reads as a
|
|
8
|
+
* cancellation nobody asked for, and the first settlement of a submission stands (measured). Both steps can be run
|
|
9
|
+
* again over their own result: the run stays until the abort's handler ends it, so a task left half way is found
|
|
10
|
+
* parked, and finished, by the next open. A parked task that is not its conversation's run (a tool's, a compaction's)
|
|
11
|
+
* is only aborted: the run goes on without it.
|
|
12
|
+
*/
|
|
13
|
+
export async function giveUpOnParked(harness, context) {
|
|
14
|
+
const parked = (await harness.inspect(context)).tasks.filter(({ state }) => state.kind === "blocked" && state.reason === RESTART_LIMIT_REASON);
|
|
15
|
+
for (const { record } of parked) {
|
|
16
|
+
await harness.commit(async (tx) => {
|
|
17
|
+
const run = (await tx.doc(LiveDoc, record.conversationId))?.run;
|
|
18
|
+
if (run?.taskId !== record.id)
|
|
19
|
+
return;
|
|
20
|
+
for (const id of run.inputs) {
|
|
21
|
+
tx.settleSubmission(id, { status: "unanswered", reason: RESTART_LIMIT_REASON, detail: DETAIL });
|
|
22
|
+
}
|
|
23
|
+
}, context);
|
|
24
|
+
await harness.abortTask(record.id, context);
|
|
25
|
+
log.warn(`[fastagent] gave up on ${record.kind} task ${record.id} of conversation ${record.conversationId}: it was parked`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Temporary: pi-durable
|
|
2
|
+
* Temporary: pi-durable leaves a conversation's queued messages waiting after a run that did not answer (a model
|
|
3
3
|
* failure, an abort, a message sent while an aborted run was still ending) until another submission places them, while
|
|
4
4
|
* the Agent API takes them whatever the message before them ended with (SPEC.md §5.3). A write of FastAgent's own
|
|
5
|
-
* kind places them.
|
|
5
|
+
* kind places them. pi-durable 1.4.0's `settings.afterFailedRun` covers a model failure (`model_error`, `no_model`)
|
|
6
|
+
* and not an abort: remove this once it does.
|
|
6
7
|
*/
|
|
7
8
|
import type { Context } from "@earendil-works/chord/context";
|
|
8
9
|
import { type Harness } from "@earendil-works/pi-durable";
|
|
@@ -33,8 +33,10 @@ export async function openLockedStorage(path, locking, holders) {
|
|
|
33
33
|
return await SqliteStorage.open(adapter);
|
|
34
34
|
}
|
|
35
35
|
catch (error) {
|
|
36
|
-
// The connection only: the adapter's close checkpoints first, which a refused opener cannot do.
|
|
37
|
-
database
|
|
36
|
+
// The connection only: the adapter's close checkpoints first, which a refused opener cannot do. Already closed
|
|
37
|
+
// when the storage refused the database itself (a schema it does not know), and closing it again would hide why.
|
|
38
|
+
if (database.isOpen)
|
|
39
|
+
database.close();
|
|
38
40
|
if (error.errcode !== SQLITE_BUSY)
|
|
39
41
|
throw error;
|
|
40
42
|
const who = holders === undefined ? "" : `: ${holders} is running`;
|
package/dist/http/contract.d.ts
CHANGED
|
@@ -464,6 +464,9 @@ declare const AgentHttpApi_base: HttpApi.HttpApi<"fastagent", HttpApiGroup.HttpA
|
|
|
464
464
|
readonly event: Schema.String;
|
|
465
465
|
readonly data: Schema.String;
|
|
466
466
|
}>, never, never>, Schema.Never, {
|
|
467
|
+
readonly type: "message";
|
|
468
|
+
readonly message: Message;
|
|
469
|
+
} | {
|
|
467
470
|
readonly type: "entry";
|
|
468
471
|
readonly entry: Entry;
|
|
469
472
|
} | {
|
|
@@ -499,9 +502,6 @@ declare const AgentHttpApi_base: HttpApi.HttpApi<"fastagent", HttpApiGroup.HttpA
|
|
|
499
502
|
readonly retryable: boolean;
|
|
500
503
|
readonly details?: Schema.Json | undefined;
|
|
501
504
|
};
|
|
502
|
-
} | {
|
|
503
|
-
readonly type: "message";
|
|
504
|
-
readonly message: Message;
|
|
505
505
|
} | {
|
|
506
506
|
readonly type: string;
|
|
507
507
|
}> | Schema.toCodecJson<typeof Message>, Schema.toCodecJson<Schema.decodeTo<Schema.declare<AgentError, AgentError>, Schema.Struct<{
|
package/dist/schedule/cron.d.ts
CHANGED
|
@@ -18,7 +18,9 @@ export declare function cronError(cron: string, tz: string | undefined): string
|
|
|
18
18
|
* A function of the expression alone, so a file's verdict never flips with the time it is read. Two consecutive
|
|
19
19
|
* instants under 10 minutes apart are on one day or across one midnight, and the time-of-day fields repeat every day,
|
|
20
20
|
* so two days of the time-of-day form (the day fields made `*`, in UTC) hold every such gap. That is conservative:
|
|
21
|
-
* `0,55 0,23 * * 1` is refused for the 23:55 → 00:00 pair, though Tuesday never fires. A DST change
|
|
22
|
-
*
|
|
21
|
+
* `0,55 0,23 * * 1` is refused for the 23:55 → 00:00 pair, though Tuesday never fires. A DST change can bring two
|
|
22
|
+
* instants closer once a year, which this does not refuse: an expression with several times runs both passes of the
|
|
23
|
+
* hour the autumn change repeats (`0,55 1 * * *` at 01:55 and, 5 minutes on, the second 01:00), and a schedule whose
|
|
24
|
+
* session is busy skips a fire anyway. A year field is refused, since what repeats every year does not need one.
|
|
23
25
|
*/
|
|
24
26
|
export declare function recurringCronError(cron: string, tz: string | undefined): string | undefined;
|
package/dist/schedule/cron.js
CHANGED
|
@@ -45,8 +45,10 @@ const verdicts = new Map();
|
|
|
45
45
|
* A function of the expression alone, so a file's verdict never flips with the time it is read. Two consecutive
|
|
46
46
|
* instants under 10 minutes apart are on one day or across one midnight, and the time-of-day fields repeat every day,
|
|
47
47
|
* so two days of the time-of-day form (the day fields made `*`, in UTC) hold every such gap. That is conservative:
|
|
48
|
-
* `0,55 0,23 * * 1` is refused for the 23:55 → 00:00 pair, though Tuesday never fires. A DST change
|
|
49
|
-
*
|
|
48
|
+
* `0,55 0,23 * * 1` is refused for the 23:55 → 00:00 pair, though Tuesday never fires. A DST change can bring two
|
|
49
|
+
* instants closer once a year, which this does not refuse: an expression with several times runs both passes of the
|
|
50
|
+
* hour the autumn change repeats (`0,55 1 * * *` at 01:55 and, 5 minutes on, the second 01:00), and a schedule whose
|
|
51
|
+
* session is busy skips a fire anyway. A year field is refused, since what repeats every year does not need one.
|
|
50
52
|
*/
|
|
51
53
|
export function recurringCronError(cron, tz) {
|
|
52
54
|
const key = `${cron}\u0000${tz ?? ""}`;
|
|
@@ -6,9 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
8
|
* Translate a 5-field cron into EventBridge Scheduler's `cron(m h dom mon dow *)`, or say why it can't be. The two
|
|
9
|
-
* fire on the same instants in the same zone, with
|
|
10
|
-
* removes (02:30 in most of the US on that day) is skipped by EventBridge and run
|
|
11
|
-
*
|
|
9
|
+
* fire on the same instants in the same zone, with two exceptions at a DST change, both on a resident host (croner):
|
|
10
|
+
* a local time the spring change removes (02:30 in most of the US on that day) is skipped by EventBridge and run by
|
|
11
|
+
* croner at the first valid time after the gap, and a schedule with several minutes or hours (every 15 minutes) runs
|
|
12
|
+
* both passes of the hour the autumn change repeats. EventBridge's documentation says a schedule "runs only once" when
|
|
13
|
+
* the clock goes back, which is read here as once per time: once for each of the four times of an every-15-minutes
|
|
14
|
+
* schedule. That reading is not tested against a live schedule. A single time in the repeated hour runs once in both.
|
|
15
|
+
* `?` is `*`, as croner reads it and as EventBridge means it ("any").
|
|
12
16
|
*/
|
|
13
17
|
export declare function toEventBridgeCron(cron: string): {
|
|
14
18
|
expression: string;
|
|
@@ -56,23 +56,24 @@ function mapDowField(dow) {
|
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
58
|
* Translate a 5-field cron into EventBridge Scheduler's `cron(m h dom mon dow *)`, or say why it can't be. The two
|
|
59
|
-
* fire on the same instants in the same zone, with
|
|
60
|
-
* removes (02:30 in most of the US on that day) is skipped by EventBridge and run
|
|
61
|
-
*
|
|
59
|
+
* fire on the same instants in the same zone, with two exceptions at a DST change, both on a resident host (croner):
|
|
60
|
+
* a local time the spring change removes (02:30 in most of the US on that day) is skipped by EventBridge and run by
|
|
61
|
+
* croner at the first valid time after the gap, and a schedule with several minutes or hours (every 15 minutes) runs
|
|
62
|
+
* both passes of the hour the autumn change repeats. EventBridge's documentation says a schedule "runs only once" when
|
|
63
|
+
* the clock goes back, which is read here as once per time: once for each of the four times of an every-15-minutes
|
|
64
|
+
* schedule. That reading is not tested against a live schedule. A single time in the repeated hour runs once in both.
|
|
65
|
+
* `?` is `*`, as croner reads it and as EventBridge means it ("any").
|
|
62
66
|
*/
|
|
63
67
|
export function toEventBridgeCron(cron) {
|
|
64
68
|
const fields = cron.trim().split(/\s+/);
|
|
65
69
|
if (fields.length !== 5) {
|
|
66
70
|
return { error: `EventBridge supports 5-field cron only (got ${fields.length} fields)` };
|
|
67
71
|
}
|
|
68
|
-
|
|
72
|
+
// EventBridge takes `?` in the day fields only, and exactly one of them; croner reads it as `*` anywhere.
|
|
73
|
+
const [min, hour, dom, mon, dow] = fields.map((field) => (field === "?" ? "*" : field));
|
|
69
74
|
if (/[L#]/i.test(dow) || /[L#]/i.test(dom)) {
|
|
70
75
|
return { error: "L/# day forms don't translate to EventBridge numbering — set this schedule up manually" };
|
|
71
76
|
}
|
|
72
|
-
// `?` FIRST, and not as a synonym for `*`.
|
|
73
|
-
if (dom === "?" || dow === "?") {
|
|
74
|
-
return { expression: `cron(${min} ${hour} * ${mon} ? *)` };
|
|
75
|
-
}
|
|
76
77
|
if (dom !== "*" && dow !== "*") {
|
|
77
78
|
return {
|
|
78
79
|
error: "restricting BOTH day-of-month and day-of-week (cron OR semantics) is not expressible in EventBridge",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastagent-sh/fastagent",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "Vibe first. Then FastAgent: turn a local agent directory into a live service in your app, on Telegram, Slack, Feishu, or behind any channel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -89,33 +89,33 @@
|
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
91
|
"@clack/prompts": "^1.6.0",
|
|
92
|
-
"@earendil-works/chord": "1.
|
|
93
|
-
"@earendil-works/pi-ai": "1.
|
|
94
|
-
"@earendil-works/pi-durable": "1.
|
|
92
|
+
"@earendil-works/chord": "1.4.0",
|
|
93
|
+
"@earendil-works/pi-ai": "1.4.0",
|
|
94
|
+
"@earendil-works/pi-durable": "1.4.0",
|
|
95
95
|
"@effect/platform-bun": "^4.0.7",
|
|
96
96
|
"@effect/platform-node": "^4.0.7",
|
|
97
|
-
"@larksuiteoapi/node-sdk": "^1.
|
|
97
|
+
"@larksuiteoapi/node-sdk": "^1.76.0",
|
|
98
98
|
"@smithy/signature-v4": "^5.7.4",
|
|
99
|
-
"chokidar": "^
|
|
99
|
+
"chokidar": "^6.0.0",
|
|
100
100
|
"commander": "^15.0.0",
|
|
101
|
-
"croner": "^
|
|
101
|
+
"croner": "^11.0.0",
|
|
102
102
|
"effect": "^4.0.7",
|
|
103
103
|
"giget": "^3.3.0",
|
|
104
|
-
"ignore": "^7.0.
|
|
104
|
+
"ignore": "^7.0.14",
|
|
105
105
|
"proper-lockfile": "^4.1.2",
|
|
106
106
|
"undici": "^8.6.0",
|
|
107
|
-
"yaml": "^2.9.
|
|
107
|
+
"yaml": "^2.9.1"
|
|
108
108
|
},
|
|
109
109
|
"devDependencies": {
|
|
110
|
-
"@biomejs/biome": "^2.
|
|
111
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
112
|
-
"@hono/node-server": "^2.1.
|
|
110
|
+
"@biomejs/biome": "^2.6.4",
|
|
111
|
+
"@earendil-works/pi-coding-agent": "1.4.0",
|
|
112
|
+
"@hono/node-server": "^2.1.5",
|
|
113
113
|
"@types/express": "^5.0.6",
|
|
114
114
|
"@types/proper-lockfile": "^4.1.4",
|
|
115
|
-
"express": "^5.
|
|
116
|
-
"fastify": "^5.12.
|
|
117
|
-
"knip": "^6.
|
|
115
|
+
"express": "^5.3.0",
|
|
116
|
+
"fastify": "^5.12.7",
|
|
117
|
+
"knip": "^6.44.0",
|
|
118
118
|
"typescript": "^7.0.2",
|
|
119
|
-
"vitest": "^5.0.
|
|
119
|
+
"vitest": "^5.0.5"
|
|
120
120
|
}
|
|
121
121
|
}
|