@saadnvd1/agent-os 0.3.624 → 0.3.627
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/lib/chat/worker/host.ts +33 -4
- package/lib/orchestrator/tools.ts +84 -16
- package/lib/orchestrator/watcher.ts +15 -2
- package/package.json +1 -1
- package/scripts/autodeploy +4 -2
package/lib/chat/worker/host.ts
CHANGED
|
@@ -533,6 +533,26 @@ export class ChatHost {
|
|
|
533
533
|
return true;
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
+
// A blocking step moved to the background frees the agent: the turn goes
|
|
537
|
+
// on, and what's queued goes in now, in order, as a message sent mid-turn
|
|
538
|
+
// does, rather than wait for the turn to end. Only the turn it was asked
|
|
539
|
+
// in: one that ended meanwhile has sent its queue by the usual rules.
|
|
540
|
+
private sendQueuedMidTurn(turn: number): void {
|
|
541
|
+
if (
|
|
542
|
+
!this.busy() ||
|
|
543
|
+
this.turnsStarted !== turn ||
|
|
544
|
+
this.closed ||
|
|
545
|
+
this.handingOver() ||
|
|
546
|
+
this.compacting ||
|
|
547
|
+
this.compactingNow
|
|
548
|
+
)
|
|
549
|
+
return;
|
|
550
|
+
for (let next; (next = claimNext(this.session.id)); ) {
|
|
551
|
+
this.emit({ type: "queue" });
|
|
552
|
+
this.sendUser(next);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
536
556
|
private sendUser(
|
|
537
557
|
m: {
|
|
538
558
|
id: string;
|
|
@@ -677,13 +697,20 @@ export class ChatHost {
|
|
|
677
697
|
this.sendUser(cmd, true);
|
|
678
698
|
return;
|
|
679
699
|
}
|
|
700
|
+
// What the reader queued goes first: an event or agent message that
|
|
701
|
+
// reaches an idle worker (a fresh one after a restart) waits behind
|
|
702
|
+
// it rather than start a turn their next message would stop.
|
|
703
|
+
const backlog = listQueue(this.session.id).length > 0;
|
|
704
|
+
if (!this.busy() && backlog && !this.closed) {
|
|
705
|
+
enqueue(this.session.id, cmd);
|
|
706
|
+
this.emit({ type: "queue" });
|
|
707
|
+
this.sendQueued();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
680
710
|
if (this.compactFirst(cmd)) return;
|
|
681
711
|
// A compaction run for an earlier message takes nothing in: what's
|
|
682
712
|
// sent meanwhile goes behind that message.
|
|
683
|
-
if (
|
|
684
|
-
(cmd.queue || this.compacting) &&
|
|
685
|
-
(this.busy() || listQueue(this.session.id).length)
|
|
686
|
-
) {
|
|
713
|
+
if ((cmd.queue || this.compacting) && (this.busy() || backlog)) {
|
|
687
714
|
// Behind the running turn, and behind anything queued before it.
|
|
688
715
|
enqueue(this.session.id, cmd);
|
|
689
716
|
this.emit({ type: "queue" });
|
|
@@ -762,7 +789,9 @@ export class ChatHost {
|
|
|
762
789
|
case "background_task":
|
|
763
790
|
try {
|
|
764
791
|
if (!this.conversation.backgroundTask) throw new Error("unsupported");
|
|
792
|
+
const turn = this.turnsStarted;
|
|
765
793
|
const ok = await this.conversation.backgroundTask(cmd.toolUseId);
|
|
794
|
+
if (ok) this.sendQueuedMidTurn(turn);
|
|
766
795
|
this.emit({ type: "background_result", reqId: cmd.reqId, ok });
|
|
767
796
|
} catch (error) {
|
|
768
797
|
this.emit({
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The orchestrator chat's tools, as an in-process MCP server in its chat
|
|
3
3
|
* worker. Each one asks the AgentOS server, which holds the live state
|
|
4
4
|
* (chat activity, status, tasks) and enforces the workspace scope, brakes
|
|
5
|
-
* and gates, and returns its text as is.
|
|
5
|
+
* and gates, and returns its text as is. While the server restarts a call
|
|
6
|
+
* waits for it (httpToolCaller).
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { createHash } from "crypto";
|
|
@@ -17,27 +18,90 @@ import { TOOL_SHAPES, TOOLS, type ToolName } from "./tool-schemas";
|
|
|
17
18
|
|
|
18
19
|
type CallToolResult = Awaited<ReturnType<Parameters<typeof tool>[3]>>;
|
|
19
20
|
|
|
20
|
-
export type ToolCaller = (
|
|
21
|
+
export type ToolCaller = (
|
|
22
|
+
tool: ToolName,
|
|
23
|
+
args: object,
|
|
24
|
+
signal?: AbortSignal
|
|
25
|
+
) => Promise<string>;
|
|
26
|
+
|
|
27
|
+
// How long a call waits for a restarting server before it gives up. A
|
|
28
|
+
// redeploy's server is back in well under this.
|
|
29
|
+
export const RESTART_WAIT_MS = 120_000;
|
|
30
|
+
const RETRY_FIRST_MS = 250;
|
|
31
|
+
const RETRY_MAX_MS = 2000;
|
|
32
|
+
|
|
33
|
+
const refused = (error: unknown) =>
|
|
34
|
+
(error as { cause?: { code?: string } } | null)?.cause?.code ===
|
|
35
|
+
"ECONNREFUSED";
|
|
36
|
+
|
|
37
|
+
const sleep = (ms: number, signal?: AbortSignal) =>
|
|
38
|
+
new Promise<void>((resolve, reject) => {
|
|
39
|
+
if (signal?.aborted) return reject(signal.reason);
|
|
40
|
+
const timer = setTimeout(() => {
|
|
41
|
+
signal?.removeEventListener("abort", onAbort);
|
|
42
|
+
resolve();
|
|
43
|
+
}, ms);
|
|
44
|
+
const onAbort = () => {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
reject(signal?.reason);
|
|
47
|
+
};
|
|
48
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
49
|
+
});
|
|
21
50
|
|
|
22
51
|
export const TOKEN_HEADER = "x-agentos-orchestrator";
|
|
23
52
|
|
|
53
|
+
// A server that is down refused the connection and never saw the request,
|
|
54
|
+
// so the call waits for it and goes again. One the restart cut off after it
|
|
55
|
+
// was sent isn't repeated (start_task, send and the rest aren't safe to):
|
|
56
|
+
// the agent is told to check first.
|
|
24
57
|
export function httpToolCaller(
|
|
25
58
|
baseUrl: string,
|
|
26
59
|
workspaceId: string,
|
|
27
|
-
token: string
|
|
60
|
+
token: string,
|
|
61
|
+
waitMs = RESTART_WAIT_MS
|
|
28
62
|
): ToolCaller {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
63
|
+
const url = `${baseUrl}/api/workspaces/${encodeURIComponent(workspaceId)}/orchestrator/tools`;
|
|
64
|
+
return async (name, args, signal) => {
|
|
65
|
+
const deadline = Date.now() + waitMs;
|
|
66
|
+
let delay = RETRY_FIRST_MS;
|
|
67
|
+
let waiting = false;
|
|
68
|
+
for (;;) {
|
|
69
|
+
let res: Response;
|
|
70
|
+
try {
|
|
71
|
+
res = await fetch(url, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
[TOKEN_HEADER]: token,
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({ tool: name, args }),
|
|
78
|
+
signal,
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (signal?.aborted) throw error;
|
|
82
|
+
if (!refused(error))
|
|
83
|
+
throw new Error(
|
|
84
|
+
`AgentOS restarted while ${name} ran, so it may or may not have taken effect. Check (sessions, read) before doing it again.`
|
|
85
|
+
);
|
|
86
|
+
if (Date.now() + delay > deadline)
|
|
87
|
+
throw new Error(
|
|
88
|
+
`AgentOS is restarting and didn't come back in time; try ${name} again shortly.`
|
|
89
|
+
);
|
|
90
|
+
if (!waiting)
|
|
91
|
+
console.error(`${name}: AgentOS is down; waiting for it to return`);
|
|
92
|
+
waiting = true;
|
|
93
|
+
await sleep(delay, signal);
|
|
94
|
+
delay = Math.min(delay * 2, RETRY_MAX_MS);
|
|
95
|
+
continue;
|
|
36
96
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
97
|
+
const text = await res.text().catch(() => {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`AgentOS restarted while ${name} ran, so it may or may not have taken effect. Check (sessions, read) before doing it again.`
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
if (!res.ok) throw new Error(text || `AgentOS answered ${res.status}`);
|
|
103
|
+
return text;
|
|
104
|
+
}
|
|
41
105
|
};
|
|
42
106
|
}
|
|
43
107
|
|
|
@@ -138,8 +202,12 @@ export function orchestratorTools(
|
|
|
138
202
|
name: ORCHESTRATOR_SERVER,
|
|
139
203
|
version: "1.0.0",
|
|
140
204
|
tools: TOOLS.map((name) =>
|
|
141
|
-
tool(
|
|
142
|
-
|
|
205
|
+
tool(
|
|
206
|
+
name,
|
|
207
|
+
DESCRIPTIONS[name],
|
|
208
|
+
TOOL_SHAPES[name],
|
|
209
|
+
(args: object, extra: unknown) =>
|
|
210
|
+
answer(call(name, args, (extra as { signal?: AbortSignal })?.signal))
|
|
143
211
|
)
|
|
144
212
|
),
|
|
145
213
|
});
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* every 15s while anything in the workspace is working, every 60s when not.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { chatState, sendChat } from "../chat/runner";
|
|
8
|
+
import { chatState, chatStateNow, sendChat } from "../chat/runner";
|
|
9
|
+
import type { ChatState } from "../chat/events";
|
|
9
10
|
import { getWorkspace } from "../workspaces";
|
|
10
11
|
import { conditionsFor, recentlyMessaged, stackFacts } from "./conditions";
|
|
11
12
|
import { deliverEvents } from "./deliver";
|
|
@@ -113,6 +114,18 @@ export function nextDiffAt(
|
|
|
113
114
|
return Math.min(now + (working ? DIFF_BUSY_MS : DIFF_IDLE_MS), ...settles);
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
// Right after a restart nothing is attached yet, though the orchestrator's
|
|
118
|
+
// worker may still be mid-turn: ask the worker, and count one that can't
|
|
119
|
+
// be reached as busy for this tick.
|
|
120
|
+
export async function deliveryTurn(
|
|
121
|
+
orchestratorId: string
|
|
122
|
+
): Promise<ChatState | null> {
|
|
123
|
+
return (
|
|
124
|
+
chatState(orchestratorId) ??
|
|
125
|
+
(await chatStateNow(orchestratorId).catch(() => "running" as const))
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
116
129
|
async function tick(): Promise<void> {
|
|
117
130
|
if (busy) return;
|
|
118
131
|
busy = true;
|
|
@@ -136,7 +149,7 @@ async function tick(): Promise<void> {
|
|
|
136
149
|
await deliverEvents({
|
|
137
150
|
workspaceId,
|
|
138
151
|
orchestratorId: orch.id,
|
|
139
|
-
turn:
|
|
152
|
+
turn: await deliveryTurn(orch.id),
|
|
140
153
|
send: sendChat,
|
|
141
154
|
});
|
|
142
155
|
} catch (error) {
|
package/package.json
CHANGED
package/scripts/autodeploy
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Run every minute by serviceman on Saad's Mac: when origin/main moves,
|
|
3
3
|
# redeploy the live instance and publish the same commit to npm. Runs in the
|
|
4
|
-
# background. Chat turns live in workers and
|
|
5
|
-
#
|
|
4
|
+
# background. Chat turns (tasks' and orchestrators') live in workers and
|
|
5
|
+
# outlive the restart, and an orchestrator's tool calls wait for the server to
|
|
6
|
+
# return. Reviews and task setups are server jobs that run again after a
|
|
7
|
+
# restart, so it waits up to 10 minutes for them rather than pay twice.
|
|
6
8
|
# sm add agentos-autodeploy "scripts/autodeploy" --cron "* * * * *"
|
|
7
9
|
# Restarts are spaced AGENTOS_MIN_GAP seconds apart (an hour; 0 turns it
|
|
8
10
|
# off): a head that lands inside the gap waits and is deployed, whatever it
|