@alisio/core 0.2.0 → 0.2.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/application.js +19 -2
- package/dist/changelog/data.js +15 -0
- package/dist/core/human-wait.d.ts +25 -0
- package/dist/core/human-wait.js +54 -0
- package/dist/core/run-clock.d.ts +47 -0
- package/dist/core/run-clock.js +111 -0
- package/dist/core/run-stats.d.ts +2 -0
- package/dist/core/runner.d.ts +12 -0
- package/dist/core/runner.js +68 -18
- package/dist/core/timeout.js +2 -2
- package/dist/goal/host.d.ts +2 -0
- package/dist/goal/host.js +2 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/plugins/host.d.ts +10 -1
- package/dist/plugins/host.js +17 -2
- package/package.json +2 -2
package/dist/application.js
CHANGED
|
@@ -16,6 +16,7 @@ import { subagentTasks } from "./background/mirror.js";
|
|
|
16
16
|
import { BackgroundTasks } from "./background/service.js";
|
|
17
17
|
import { configFile, configHome, loadConfigWithProvenance, overridesSavedProviderProfile, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.js";
|
|
18
18
|
import { completeText } from "./core/compaction.js";
|
|
19
|
+
import { HumanWaits } from "./core/human-wait.js";
|
|
19
20
|
import { DEFAULT_MAX_OUTPUT_TOKENS } from "./core/output-limit.js";
|
|
20
21
|
import { ToolRegistry } from "./core/registry.js";
|
|
21
22
|
import { AgentRunner } from "./core/runner.js";
|
|
@@ -86,11 +87,25 @@ export async function createApplication(options = {}) {
|
|
|
86
87
|
const addDirs = options.readOnly
|
|
87
88
|
? []
|
|
88
89
|
: await Promise.all((options.addDirs ?? []).map((dir) => realpath(resolve(cwd, dir)).catch(() => resolve(cwd, dir))));
|
|
90
|
+
// Waits for a person (approvals, questions, the plan review) do not count against the run's
|
|
91
|
+
// time limit. One registry serves the runner, the plugin host's interactive UI and the
|
|
92
|
+
// external-directory approval, so every host (TUI, web server, embedders) gets it for free.
|
|
93
|
+
const humanWaits = new HumanWaits((session) => {
|
|
94
|
+
try {
|
|
95
|
+
return store.get(session).parentId ?? undefined;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const askExternalDirectory = options.approveExternalDirectory;
|
|
89
102
|
const pathAccess = new PathAccess({
|
|
90
103
|
workspace,
|
|
91
104
|
extraRoots: [...config.additionalDirectories, ...addDirs],
|
|
92
|
-
...(
|
|
93
|
-
? {
|
|
105
|
+
...(askExternalDirectory && !options.readOnly
|
|
106
|
+
? {
|
|
107
|
+
approve: (request) => humanWaits.track(request.session, () => askExternalDirectory(request)),
|
|
108
|
+
}
|
|
94
109
|
: {}),
|
|
95
110
|
readOnly: !!options.readOnly,
|
|
96
111
|
});
|
|
@@ -730,7 +745,9 @@ export async function createApplication(options = {}) {
|
|
|
730
745
|
: [];
|
|
731
746
|
return declared.length ? Math.min(...declared) : undefined;
|
|
732
747
|
};
|
|
748
|
+
plugins.setHumanWaits(humanWaits);
|
|
733
749
|
const runner = new AgentRunner({
|
|
750
|
+
humanWaits,
|
|
734
751
|
provider,
|
|
735
752
|
providerFor,
|
|
736
753
|
registry,
|
package/dist/changelog/data.js
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
export const CHANGELOG_DATA = {
|
|
3
3
|
schema: 1,
|
|
4
4
|
entries: [
|
|
5
|
+
{
|
|
6
|
+
version: "0.2.1",
|
|
7
|
+
date: "2026-10-02",
|
|
8
|
+
sections: [
|
|
9
|
+
{
|
|
10
|
+
title: "Fixed",
|
|
11
|
+
items: [
|
|
12
|
+
'A run no longer stops with "reached its 600 s limit" while you read a plan or answer a question. The run time limit (`limits.timeoutMs`) now counts only active time: the time the agent waits for you (the plan review, a question, a tool approval) is not counted. A model or tool that really stalls still hits the limit. The same applies to the `goal.maxMinutes` cap of a session goal.',
|
|
13
|
+
'A run that ended or was stopped during a plan review no longer leaves the next message failing with an "Uncertain tool outcome" error.',
|
|
14
|
+
"In the web, a plan review or a question can wait up to 60 minutes for your answer (tool approvals still expire after 10 minutes).",
|
|
15
|
+
'The Settings label of `limits.timeoutMs` said "Provider request timeout"; it now says it is the run time limit.',
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
5
20
|
{
|
|
6
21
|
version: "0.2.0",
|
|
7
22
|
date: "2026-10-02",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human waits: the moments a run is blocked on a PERSON (a tool approval, an external-directory
|
|
3
|
+
* approval, `ask_user_question`, the plan review) and not on work. The run's time limit does not
|
|
4
|
+
* count them (see `RunClock`).
|
|
5
|
+
*
|
|
6
|
+
* One registry per application. Each active run registers its clock under its session; anything
|
|
7
|
+
* that asks a person goes through `track`, which pauses the clocks involved until the answer
|
|
8
|
+
* arrives. A child session (a subagent) pauses its own clock AND its ancestors': a parent that
|
|
9
|
+
* waits for a child is doing real work, but when that child waits for a person nobody works.
|
|
10
|
+
*/
|
|
11
|
+
import type { RunClock } from "./run-clock.ts";
|
|
12
|
+
export declare class HumanWaits {
|
|
13
|
+
private readonly parentOf;
|
|
14
|
+
private clocks;
|
|
15
|
+
constructor(parentOf?: (session: string) => string | undefined);
|
|
16
|
+
/** A run began: its clock follows the waits of its session. Returns the unregister function. */
|
|
17
|
+
register(session: string, clock: RunClock): () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Opens a wait for `session` (the session asking; its ancestors are paused too). Without a
|
|
20
|
+
* session every active run is paused: somebody is being asked and nobody can say who for.
|
|
21
|
+
*/
|
|
22
|
+
begin(session?: string): () => void;
|
|
23
|
+
/** Runs `wait` as a human wait: the clocks are paused until it settles, however it settles. */
|
|
24
|
+
track<T>(session: string | undefined, wait: () => Promise<T>): Promise<T>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export class HumanWaits {
|
|
2
|
+
parentOf;
|
|
3
|
+
clocks = new Map();
|
|
4
|
+
constructor(parentOf = () => undefined) {
|
|
5
|
+
this.parentOf = parentOf;
|
|
6
|
+
}
|
|
7
|
+
/** A run began: its clock follows the waits of its session. Returns the unregister function. */
|
|
8
|
+
register(session, clock) {
|
|
9
|
+
this.clocks.set(session, clock);
|
|
10
|
+
return () => {
|
|
11
|
+
if (this.clocks.get(session) === clock)
|
|
12
|
+
this.clocks.delete(session);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Opens a wait for `session` (the session asking; its ancestors are paused too). Without a
|
|
17
|
+
* session every active run is paused: somebody is being asked and nobody can say who for.
|
|
18
|
+
*/
|
|
19
|
+
begin(session) {
|
|
20
|
+
const releases = [];
|
|
21
|
+
if (session === undefined)
|
|
22
|
+
for (const clock of this.clocks.values())
|
|
23
|
+
releases.push(clock.pause());
|
|
24
|
+
else {
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (let id = session; id !== undefined && !seen.has(id);) {
|
|
27
|
+
seen.add(id);
|
|
28
|
+
const clock = this.clocks.get(id);
|
|
29
|
+
if (clock)
|
|
30
|
+
releases.push(clock.pause());
|
|
31
|
+
try {
|
|
32
|
+
id = this.parentOf(id);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
id = undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return () => {
|
|
40
|
+
for (const release of releases)
|
|
41
|
+
release();
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Runs `wait` as a human wait: the clocks are paused until it settles, however it settles. */
|
|
45
|
+
async track(session, wait) {
|
|
46
|
+
const end = this.begin(session);
|
|
47
|
+
try {
|
|
48
|
+
return await wait();
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
end();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run's time limit (`limits.timeoutMs`) as a PAUSABLE clock.
|
|
3
|
+
*
|
|
4
|
+
* The limit exists to stop runaway work (a stuck model, a looping tool), not to cap how long a
|
|
5
|
+
* person takes to read a plan or answer a question. So the clock counts ACTIVE time only: while at
|
|
6
|
+
* least one human wait is open (an approval, a question, a plan review) it is paused, and it
|
|
7
|
+
* resumes with the remaining time when the last wait ends.
|
|
8
|
+
*
|
|
9
|
+
* Waits nest and overlap (several tools can wait for approvals in parallel), so pausing is
|
|
10
|
+
* counted: the clock runs again only when every pause was released. A pause is released exactly
|
|
11
|
+
* once (the function `pause()` returns is idempotent), and a released or disposed clock ignores
|
|
12
|
+
* late calls, so an aborted run can never leave a timer behind.
|
|
13
|
+
*/
|
|
14
|
+
/** Time source and timers, replaceable in tests. */
|
|
15
|
+
export interface ClockSource {
|
|
16
|
+
now(): number;
|
|
17
|
+
setTimeout(fn: () => void, ms: number): unknown;
|
|
18
|
+
clearTimeout(handle: unknown): void;
|
|
19
|
+
}
|
|
20
|
+
export declare class RunClock {
|
|
21
|
+
readonly limitMs: number;
|
|
22
|
+
private readonly controller;
|
|
23
|
+
private readonly source;
|
|
24
|
+
private consumed;
|
|
25
|
+
/** Start of the current running stretch (undefined while paused or finished). */
|
|
26
|
+
private since;
|
|
27
|
+
private timer;
|
|
28
|
+
private pauses;
|
|
29
|
+
private done;
|
|
30
|
+
constructor(limitMs: number, source?: ClockSource);
|
|
31
|
+
/** Aborts with a `TimeoutError` (the same reason `AbortSignal.timeout` uses) at the limit. */
|
|
32
|
+
get signal(): AbortSignal;
|
|
33
|
+
/** Whether at least one human wait is open. */
|
|
34
|
+
get paused(): boolean;
|
|
35
|
+
/** Time the run was really working: elapsed time minus every paused stretch. */
|
|
36
|
+
activeMs(): number;
|
|
37
|
+
remainingMs(): number;
|
|
38
|
+
/**
|
|
39
|
+
* Opens a human wait. Returns the function that closes it (idempotent). Pausing a clock that
|
|
40
|
+
* already ended (limit reached or disposed) does nothing.
|
|
41
|
+
*/
|
|
42
|
+
pause(): () => void;
|
|
43
|
+
/** Stops the clock for good and frees its timer (call when the run ends, whatever the reason). */
|
|
44
|
+
dispose(): void;
|
|
45
|
+
private arm;
|
|
46
|
+
private disarm;
|
|
47
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run's time limit (`limits.timeoutMs`) as a PAUSABLE clock.
|
|
3
|
+
*
|
|
4
|
+
* The limit exists to stop runaway work (a stuck model, a looping tool), not to cap how long a
|
|
5
|
+
* person takes to read a plan or answer a question. So the clock counts ACTIVE time only: while at
|
|
6
|
+
* least one human wait is open (an approval, a question, a plan review) it is paused, and it
|
|
7
|
+
* resumes with the remaining time when the last wait ends.
|
|
8
|
+
*
|
|
9
|
+
* Waits nest and overlap (several tools can wait for approvals in parallel), so pausing is
|
|
10
|
+
* counted: the clock runs again only when every pause was released. A pause is released exactly
|
|
11
|
+
* once (the function `pause()` returns is idempotent), and a released or disposed clock ignores
|
|
12
|
+
* late calls, so an aborted run can never leave a timer behind.
|
|
13
|
+
*/
|
|
14
|
+
/** Longest delay a timer supports; a longer limit re-arms itself. */
|
|
15
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
16
|
+
const realSource = {
|
|
17
|
+
now: () => Date.now(),
|
|
18
|
+
setTimeout: (fn, ms) => {
|
|
19
|
+
const timer = setTimeout(fn, ms);
|
|
20
|
+
// Like `AbortSignal.timeout`: the limit alone never keeps the process alive.
|
|
21
|
+
timer.unref?.();
|
|
22
|
+
return timer;
|
|
23
|
+
},
|
|
24
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
25
|
+
};
|
|
26
|
+
export class RunClock {
|
|
27
|
+
limitMs;
|
|
28
|
+
controller = new AbortController();
|
|
29
|
+
source;
|
|
30
|
+
consumed = 0;
|
|
31
|
+
/** Start of the current running stretch (undefined while paused or finished). */
|
|
32
|
+
since;
|
|
33
|
+
timer;
|
|
34
|
+
pauses = 0;
|
|
35
|
+
done = false;
|
|
36
|
+
constructor(limitMs, source = realSource) {
|
|
37
|
+
this.limitMs = limitMs;
|
|
38
|
+
this.source = source;
|
|
39
|
+
this.since = source.now();
|
|
40
|
+
this.arm();
|
|
41
|
+
}
|
|
42
|
+
/** Aborts with a `TimeoutError` (the same reason `AbortSignal.timeout` uses) at the limit. */
|
|
43
|
+
get signal() {
|
|
44
|
+
return this.controller.signal;
|
|
45
|
+
}
|
|
46
|
+
/** Whether at least one human wait is open. */
|
|
47
|
+
get paused() {
|
|
48
|
+
return this.pauses > 0;
|
|
49
|
+
}
|
|
50
|
+
/** Time the run was really working: elapsed time minus every paused stretch. */
|
|
51
|
+
activeMs() {
|
|
52
|
+
return (this.consumed + (this.since === undefined ? 0 : Math.max(0, this.source.now() - this.since)));
|
|
53
|
+
}
|
|
54
|
+
remainingMs() {
|
|
55
|
+
return Math.max(0, this.limitMs - this.activeMs());
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Opens a human wait. Returns the function that closes it (idempotent). Pausing a clock that
|
|
59
|
+
* already ended (limit reached or disposed) does nothing.
|
|
60
|
+
*/
|
|
61
|
+
pause() {
|
|
62
|
+
if (this.done)
|
|
63
|
+
return () => { };
|
|
64
|
+
if (this.pauses++ === 0) {
|
|
65
|
+
if (this.since !== undefined)
|
|
66
|
+
this.consumed += Math.max(0, this.source.now() - this.since);
|
|
67
|
+
this.since = undefined;
|
|
68
|
+
this.disarm();
|
|
69
|
+
}
|
|
70
|
+
let released = false;
|
|
71
|
+
return () => {
|
|
72
|
+
if (released)
|
|
73
|
+
return;
|
|
74
|
+
released = true;
|
|
75
|
+
if (this.done)
|
|
76
|
+
return;
|
|
77
|
+
if (--this.pauses === 0) {
|
|
78
|
+
this.since = this.source.now();
|
|
79
|
+
this.arm();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Stops the clock for good and frees its timer (call when the run ends, whatever the reason). */
|
|
84
|
+
dispose() {
|
|
85
|
+
if (this.done)
|
|
86
|
+
return;
|
|
87
|
+
this.consumed = this.activeMs();
|
|
88
|
+
this.done = true;
|
|
89
|
+
this.since = undefined;
|
|
90
|
+
this.disarm();
|
|
91
|
+
}
|
|
92
|
+
arm() {
|
|
93
|
+
this.disarm();
|
|
94
|
+
this.timer = this.source.setTimeout(() => {
|
|
95
|
+
this.timer = undefined;
|
|
96
|
+
if (this.done || this.pauses > 0)
|
|
97
|
+
return;
|
|
98
|
+
if (this.remainingMs() > 0)
|
|
99
|
+
return this.arm();
|
|
100
|
+
this.consumed = this.activeMs();
|
|
101
|
+
this.done = true;
|
|
102
|
+
this.since = undefined;
|
|
103
|
+
this.controller.abort(new DOMException("The operation timed out.", "TimeoutError"));
|
|
104
|
+
}, Math.min(this.remainingMs(), MAX_TIMER_MS));
|
|
105
|
+
}
|
|
106
|
+
disarm() {
|
|
107
|
+
if (this.timer !== undefined)
|
|
108
|
+
this.source.clearTimeout(this.timer);
|
|
109
|
+
this.timer = undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/core/run-stats.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface RunStats {
|
|
|
11
11
|
output: number;
|
|
12
12
|
};
|
|
13
13
|
toolCalls: number;
|
|
14
|
+
/** Active time of the run (waits for a person excluded). */
|
|
15
|
+
activeMs?: number;
|
|
14
16
|
}
|
|
15
17
|
export declare function attachRunStats(error: unknown, stats: RunStats): void;
|
|
16
18
|
export declare function runStatsOf(error: unknown): RunStats | undefined;
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Attachment, type DatasetRef, type Message, type ModelProvider, type RunEvent, type ToolDefinition, type ToolResult } from "@alisio/sdk";
|
|
2
2
|
import type { PathAccess } from "../runtime/access.ts";
|
|
3
3
|
import type { ApprovalHandler, ArtifactPublisherFactory, CapabilityGate, ContextSource, Policy, RunnerExtensions, SessionStore } from "./contracts.ts";
|
|
4
|
+
import { HumanWaits } from "./human-wait.ts";
|
|
4
5
|
import type { ToolRegistry } from "./registry.ts";
|
|
5
6
|
export interface CompactionSettings {
|
|
6
7
|
/** Compact automatically when context usage crosses `threshold` of a known window. */
|
|
@@ -62,6 +63,13 @@ export interface RunnerOptions {
|
|
|
62
63
|
compaction?: CompactionSettings;
|
|
63
64
|
/** When set, write/process tools not allowed by the policy are offered and ask first. */
|
|
64
65
|
approve?: ApprovalHandler;
|
|
66
|
+
/**
|
|
67
|
+
* Registry of the waits for a person (approvals, questions, the plan review): the run's time
|
|
68
|
+
* limit does not count them. Share one with the plugin host and every other asking surface so a
|
|
69
|
+
* wait outside the runner (for example `ask_user_question`) pauses the same clocks. Without it
|
|
70
|
+
* the runner uses a private one, which still covers every approval it asks itself.
|
|
71
|
+
*/
|
|
72
|
+
humanWaits?: HumanWaits;
|
|
65
73
|
/**
|
|
66
74
|
* Mediated path policy (workspace plus declared extra directories and external approval).
|
|
67
75
|
* When set, declared tool paths outside the allowed roots ask before the effect gate runs.
|
|
@@ -179,6 +187,8 @@ export declare class AgentRunner {
|
|
|
179
187
|
private inbox;
|
|
180
188
|
/** Last provider-reported context size per session and the history length it covered. */
|
|
181
189
|
private reported;
|
|
190
|
+
/** Where human waits are opened; the clocks of the active runs listen to it. */
|
|
191
|
+
readonly humanWaits: HumanWaits;
|
|
182
192
|
constructor(options: RunnerOptions);
|
|
183
193
|
/** Current capability policy (may widen when the user approves an effect for the session). */
|
|
184
194
|
get policy(): Readonly<Policy>;
|
|
@@ -284,5 +294,7 @@ export declare class AgentRunner {
|
|
|
284
294
|
};
|
|
285
295
|
/** Tool calls the model made during the run (the goal controller's no-progress breaker). */
|
|
286
296
|
toolCalls: number;
|
|
297
|
+
/** Time the run was really working: wall time minus the waits for a person. */
|
|
298
|
+
activeMs: number;
|
|
287
299
|
}>;
|
|
288
300
|
}
|
package/dist/core/runner.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { isEphemeralRunEventType, textProjection, textResult, } from "@alisio/sdk";
|
|
2
2
|
import { checkpointInstructions, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, reduceMessagesToBudget, shouldCompactContext, summarize, summaryMessage, } from "./compaction.js";
|
|
3
|
+
import { HumanWaits } from "./human-wait.js";
|
|
3
4
|
import { EXIT_PLAN_TOOL, optInAllows } from "./opt-in.js";
|
|
4
5
|
import { resolveMaxOutputTokens } from "./output-limit.js";
|
|
6
|
+
import { RunClock } from "./run-clock.js";
|
|
5
7
|
import { attachRunStats } from "./run-stats.js";
|
|
6
8
|
import { isTimeoutReason, providerLabel, RunTimeoutError } from "./timeout.js";
|
|
7
9
|
import { incompleteCalls, isOutputTruncationError, lowerEffort, RunTruncationError, TRUNCATION_NOTICE, } from "./truncation.js";
|
|
@@ -60,8 +62,20 @@ export class AgentRunner {
|
|
|
60
62
|
inbox = new Map();
|
|
61
63
|
/** Last provider-reported context size per session and the history length it covered. */
|
|
62
64
|
reported = new Map();
|
|
65
|
+
/** Where human waits are opened; the clocks of the active runs listen to it. */
|
|
66
|
+
humanWaits;
|
|
63
67
|
constructor(options) {
|
|
64
68
|
this.options = options;
|
|
69
|
+
this.humanWaits =
|
|
70
|
+
options.humanWaits ??
|
|
71
|
+
new HumanWaits((session) => {
|
|
72
|
+
try {
|
|
73
|
+
return options.store.get(session).parentId ?? undefined;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
65
79
|
}
|
|
66
80
|
/** Current capability policy (may widen when the user approves an effect for the session). */
|
|
67
81
|
get policy() {
|
|
@@ -465,17 +479,20 @@ export class AgentRunner {
|
|
|
465
479
|
...(options.label ? { label: options.label } : {}),
|
|
466
480
|
...(capability ? { capability } : {}),
|
|
467
481
|
});
|
|
468
|
-
const
|
|
482
|
+
const approve = o.approve;
|
|
483
|
+
const callInput = p.input;
|
|
484
|
+
// Waiting for the person is not work: the run's time limit does not count it.
|
|
485
|
+
const decision = await this.humanWaits.track(sessionId, () => approve({
|
|
469
486
|
call,
|
|
470
487
|
effect,
|
|
471
|
-
input:
|
|
488
|
+
input: callInput,
|
|
472
489
|
signal: combined,
|
|
473
490
|
session: sessionId,
|
|
474
491
|
...(options.label ? { label: options.label } : {}),
|
|
475
492
|
...(capability ? { capability } : {}),
|
|
476
493
|
...(preview !== undefined ? { preview } : {}),
|
|
477
494
|
...(capability && gate?.runtime ? { runtime: gate.runtime } : {}),
|
|
478
|
-
});
|
|
495
|
+
}));
|
|
479
496
|
combined.throwIfAborted();
|
|
480
497
|
const persisted = !!capability && !!gate && decision === "session";
|
|
481
498
|
if (capability && gate)
|
|
@@ -530,7 +547,8 @@ export class AgentRunner {
|
|
|
530
547
|
});
|
|
531
548
|
let decision = "deny";
|
|
532
549
|
try {
|
|
533
|
-
|
|
550
|
+
const approve = o.approve;
|
|
551
|
+
decision = await this.humanWaits.track(sessionId, () => approve({
|
|
534
552
|
call: installCall,
|
|
535
553
|
effect: "process",
|
|
536
554
|
input: { extras: install.extras },
|
|
@@ -539,7 +557,7 @@ export class AgentRunner {
|
|
|
539
557
|
...(options.label ? { label: options.label } : {}),
|
|
540
558
|
capability: "analysis.install",
|
|
541
559
|
install,
|
|
542
|
-
});
|
|
560
|
+
}));
|
|
543
561
|
}
|
|
544
562
|
finally {
|
|
545
563
|
combined.throwIfAborted();
|
|
@@ -607,8 +625,12 @@ export class AgentRunner {
|
|
|
607
625
|
}
|
|
608
626
|
catch (error) {
|
|
609
627
|
result = textResult(error instanceof Error ? error.message : String(error), true);
|
|
610
|
-
// A cancellation after execution may have left effects
|
|
611
|
-
|
|
628
|
+
// A cancellation after execution may have left effects, so the journal stays pending for
|
|
629
|
+
// the user to inspect. A `read` call cannot have left any (and `exit_plan`, which is one,
|
|
630
|
+
// is exactly what a stop or the run limit interrupts while a person decides): it is closed
|
|
631
|
+
// with the cancellation, so the next message continues normally instead of asking to
|
|
632
|
+
// "recover" a call nobody needs to inspect.
|
|
633
|
+
if (!combined.aborted || p.tool?.effect === "read")
|
|
612
634
|
o.store.endCall(sessionId, call, result);
|
|
613
635
|
}
|
|
614
636
|
emit("tool_completed", {
|
|
@@ -649,10 +671,11 @@ export class AgentRunner {
|
|
|
649
671
|
}
|
|
650
672
|
};
|
|
651
673
|
const runTimeoutMs = options.timeoutMs ?? o.timeoutMs ?? 300_000;
|
|
652
|
-
const
|
|
674
|
+
const clock = new RunClock(runTimeoutMs);
|
|
675
|
+
const unregisterClock = this.humanWaits.register(sessionId, clock);
|
|
653
676
|
const combined = AbortSignal.any([
|
|
654
677
|
controller.signal,
|
|
655
|
-
|
|
678
|
+
clock.signal,
|
|
656
679
|
...(options.signal ? [options.signal] : []),
|
|
657
680
|
]);
|
|
658
681
|
const workspace = options.workspace ?? o.workspace;
|
|
@@ -730,6 +753,8 @@ export class AgentRunner {
|
|
|
730
753
|
throw error;
|
|
731
754
|
}
|
|
732
755
|
finally {
|
|
756
|
+
unregisterClock();
|
|
757
|
+
clock.dispose();
|
|
733
758
|
if (acquired)
|
|
734
759
|
o.store.release(sessionId);
|
|
735
760
|
this.active.delete(sessionId);
|
|
@@ -760,9 +785,14 @@ export class AgentRunner {
|
|
|
760
785
|
/** Continuation notice and lowered effort for the request that follows a cut-off response. */
|
|
761
786
|
let recoveryNotice;
|
|
762
787
|
let recoveryEffort;
|
|
763
|
-
|
|
764
|
-
const
|
|
765
|
-
const
|
|
788
|
+
// Active time only: waits for a person pause it (see `RunClock`).
|
|
789
|
+
const clock = new RunClock(runTimeoutMs);
|
|
790
|
+
const unregisterClock = this.humanWaits.register(sessionId, clock);
|
|
791
|
+
const combined = AbortSignal.any([
|
|
792
|
+
controller.signal,
|
|
793
|
+
clock.signal,
|
|
794
|
+
...(signal ? [signal] : []),
|
|
795
|
+
]);
|
|
766
796
|
// What the run was doing, so a timeout can say where it was stuck (see `RunTimeoutError`).
|
|
767
797
|
let stage = "other";
|
|
768
798
|
let stageTool;
|
|
@@ -1006,7 +1036,7 @@ export class AgentRunner {
|
|
|
1006
1036
|
}
|
|
1007
1037
|
// Do not start an attempt the whole-run limit could not let finish.
|
|
1008
1038
|
const retryPauseMs = FIRST_TOKEN_RETRY_PAUSE_MS;
|
|
1009
|
-
if (
|
|
1039
|
+
if (clock.remainingMs() < retryPauseMs + firstTokenMs) {
|
|
1010
1040
|
if (failure !== undefined)
|
|
1011
1041
|
throw failure;
|
|
1012
1042
|
break;
|
|
@@ -1118,7 +1148,14 @@ export class AgentRunner {
|
|
|
1118
1148
|
}
|
|
1119
1149
|
emit("run_completed", final);
|
|
1120
1150
|
finish("completed");
|
|
1121
|
-
return {
|
|
1151
|
+
return {
|
|
1152
|
+
sessionId,
|
|
1153
|
+
text: lastText,
|
|
1154
|
+
status: "completed",
|
|
1155
|
+
usage: usageTotal,
|
|
1156
|
+
toolCalls,
|
|
1157
|
+
activeMs: clock.activeMs(),
|
|
1158
|
+
};
|
|
1122
1159
|
}
|
|
1123
1160
|
toolCalls += completion.calls.length;
|
|
1124
1161
|
const prepared = completion.calls.map((call) => {
|
|
@@ -1209,7 +1246,14 @@ export class AgentRunner {
|
|
|
1209
1246
|
// token budget (`maxTokens`) and the timeout; the turn count is a safety rail.
|
|
1210
1247
|
emit("run_turns_exceeded", { turns: maxTurns, maxTurns });
|
|
1211
1248
|
finish("turns_exceeded");
|
|
1212
|
-
return {
|
|
1249
|
+
return {
|
|
1250
|
+
sessionId,
|
|
1251
|
+
text: lastText,
|
|
1252
|
+
status: "turns-exceeded",
|
|
1253
|
+
usage: usageTotal,
|
|
1254
|
+
toolCalls,
|
|
1255
|
+
activeMs: clock.activeMs(),
|
|
1256
|
+
};
|
|
1213
1257
|
}
|
|
1214
1258
|
catch (error) {
|
|
1215
1259
|
const timedOut = silentTimedOut
|
|
@@ -1228,7 +1272,11 @@ export class AgentRunner {
|
|
|
1228
1272
|
});
|
|
1229
1273
|
emit("run_failed", { error: failure.message, code: "timeout", timeout: failure.info });
|
|
1230
1274
|
finish("failed", failure.message);
|
|
1231
|
-
attachRunStats(failure, {
|
|
1275
|
+
attachRunStats(failure, {
|
|
1276
|
+
usage: { ...usageTotal },
|
|
1277
|
+
toolCalls,
|
|
1278
|
+
activeMs: clock.activeMs(),
|
|
1279
|
+
});
|
|
1232
1280
|
throw failure;
|
|
1233
1281
|
}
|
|
1234
1282
|
if (error instanceof RunTruncationError) {
|
|
@@ -1238,7 +1286,7 @@ export class AgentRunner {
|
|
|
1238
1286
|
truncation: error.info,
|
|
1239
1287
|
});
|
|
1240
1288
|
finish("failed", error.message);
|
|
1241
|
-
attachRunStats(error, { usage: { ...usageTotal }, toolCalls });
|
|
1289
|
+
attachRunStats(error, { usage: { ...usageTotal }, toolCalls, activeMs: clock.activeMs() });
|
|
1242
1290
|
throw error;
|
|
1243
1291
|
}
|
|
1244
1292
|
// On cancellation report the abort reason, not the transport's secondary error.
|
|
@@ -1246,10 +1294,12 @@ export class AgentRunner {
|
|
|
1246
1294
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1247
1295
|
emit(combined.aborted ? "run_cancelled" : "run_failed", { error: message });
|
|
1248
1296
|
finish(combined.aborted ? "cancelled" : "failed", message);
|
|
1249
|
-
attachRunStats(error, { usage: { ...usageTotal }, toolCalls });
|
|
1297
|
+
attachRunStats(error, { usage: { ...usageTotal }, toolCalls, activeMs: clock.activeMs() });
|
|
1250
1298
|
throw error;
|
|
1251
1299
|
}
|
|
1252
1300
|
finally {
|
|
1301
|
+
unregisterClock();
|
|
1302
|
+
clock.dispose();
|
|
1253
1303
|
if (acquired)
|
|
1254
1304
|
o.store.release(sessionId);
|
|
1255
1305
|
this.active.delete(sessionId);
|
package/dist/core/timeout.js
CHANGED
|
@@ -48,7 +48,7 @@ export function describeTimeout(info) {
|
|
|
48
48
|
return info.firstRequest
|
|
49
49
|
? `${who} did not respond within ${limit}: no tokens were received. ` +
|
|
50
50
|
"Retry, switch to another model, or raise limits.timeoutMs."
|
|
51
|
-
: `The run reached its ${limit}
|
|
51
|
+
: `The run reached its limit of ${limit} of active time (time spent waiting for you is not counted) while waiting for ${info.model ? `the model ${info.model}` : "the model"}` +
|
|
52
52
|
`${info.provider ? ` (${info.provider})` : ""}. ` +
|
|
53
53
|
"Everything produced so far is kept: prompt again to continue, switch model, or raise limits.timeoutMs.";
|
|
54
54
|
const doing = info.stage === "tool" && info.tool
|
|
@@ -56,6 +56,6 @@ export function describeTimeout(info) {
|
|
|
56
56
|
: info.stage === "streaming"
|
|
57
57
|
? "while the model was answering"
|
|
58
58
|
: "";
|
|
59
|
-
return (`The run reached its ${limit}
|
|
59
|
+
return (`The run reached its limit of ${limit} of active time (time spent waiting for you is not counted)${doing ? ` ${doing}` : ""}. ` +
|
|
60
60
|
"Everything produced so far is kept: prompt again to continue, or raise limits.timeoutMs.");
|
|
61
61
|
}
|
package/dist/goal/host.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface RunSummary {
|
|
|
8
8
|
output: number;
|
|
9
9
|
};
|
|
10
10
|
toolCalls?: number;
|
|
11
|
+
/** Active time of the run: waits for a person are not counted. */
|
|
12
|
+
activeMs?: number;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* The outcome of one run. `cancelled` is whether the run's abort signal fired (the user stopped
|
package/dist/goal/host.js
CHANGED
|
@@ -30,7 +30,8 @@ export function goalOutcome(input) {
|
|
|
30
30
|
text: result?.text ?? "",
|
|
31
31
|
toolCalls: result?.toolCalls ?? stats?.toolCalls ?? 0,
|
|
32
32
|
startedAt: input.startedAt,
|
|
33
|
-
|
|
33
|
+
// `goal.maxMinutes` counts the time a run was working, never the time it waited for you.
|
|
34
|
+
durationMs: Math.max(0, Math.min(input.endedAt - input.startedAt, result?.activeMs ?? stats?.activeMs ?? Number.POSITIVE_INFINITY)),
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -34,9 +34,11 @@ export { BUILTIN_COMMANDS, type BuiltinCommand, CommandCatalog, type CommandExec
|
|
|
34
34
|
export { type Config, type ConfigLoadResult, type ConfigProvenance, configFile, configHome, configSchema, isSettableSettingKey, type LoadedConfig, loadConfig, loadConfigWithProvenance, type McpServerSource, OCI_IMAGE_PATTERN, overridesSavedProviderProfile, type SettableSettingInfo, type SettableSettingKey, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, settableSettings, stateHome, } from "./config.ts";
|
|
35
35
|
export { type ContextBudget, checkpointInstructions, completeText, effectiveContextBudget, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, shouldCompactContext, summarize, } from "./core/compaction.ts";
|
|
36
36
|
export type { ApprovalDecision, ApprovalHandler, ApprovalRequest, ArtifactPublisherFactory, BeginRunInput, CapabilityGate, ContextSource, CoreArtifactPublisher, EndRunInput, EventPage, HookFailure, MessagePage, PageOptions, Policy, PublishedArtifactInfo, RunnerExtensions, RunRecord, RunStatus, Session, SessionStore, StoredEvent, TerminalRunStatus, ToolCallMeta, } from "./core/contracts.ts";
|
|
37
|
+
export { HumanWaits } from "./core/human-wait.ts";
|
|
37
38
|
export { EXIT_PLAN_TOOL, GOAL_TOOLS, OPT_IN_TOOLS } from "./core/opt-in.ts";
|
|
38
39
|
export { DEFAULT_MAX_OUTPUT_TOKENS, describeOutputLimitSource, MAX_AUTO_OUTPUT_TOKENS, type OutputLimit, type OutputLimitSource, resolveMaxOutputTokens, } from "./core/output-limit.ts";
|
|
39
40
|
export { ToolRegistry } from "./core/registry.ts";
|
|
41
|
+
export { RunClock } from "./core/run-clock.ts";
|
|
40
42
|
export { AgentRunner, type CompactionResult, defaultTokenBudget, type RunnerOptions, type RunnerSettingsPatch, type RunOptions, } from "./core/runner.ts";
|
|
41
43
|
export { type ExtensionConflict, ExtensionRegistry } from "./extensions/registry.ts";
|
|
42
44
|
export * from "./goal/index.ts";
|
package/dist/index.js
CHANGED
|
@@ -33,9 +33,11 @@ export { changelogNews, compareVersions, formatChangelogMarkdown, isVersion, loa
|
|
|
33
33
|
export { BUILTIN_COMMANDS, CommandCatalog, } from "./commands/catalog.js";
|
|
34
34
|
export { configFile, configHome, configSchema, isSettableSettingKey, loadConfig, loadConfigWithProvenance, OCI_IMAGE_PATTERN, overridesSavedProviderProfile, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, settableSettings, stateHome, } from "./config.js";
|
|
35
35
|
export { checkpointInstructions, completeText, effectiveContextBudget, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, shouldCompactContext, summarize, } from "./core/compaction.js";
|
|
36
|
+
export { HumanWaits } from "./core/human-wait.js";
|
|
36
37
|
export { EXIT_PLAN_TOOL, GOAL_TOOLS, OPT_IN_TOOLS } from "./core/opt-in.js";
|
|
37
38
|
export { DEFAULT_MAX_OUTPUT_TOKENS, describeOutputLimitSource, MAX_AUTO_OUTPUT_TOKENS, resolveMaxOutputTokens, } from "./core/output-limit.js";
|
|
38
39
|
export { ToolRegistry } from "./core/registry.js";
|
|
40
|
+
export { RunClock } from "./core/run-clock.js";
|
|
39
41
|
export { AgentRunner, defaultTokenBudget, } from "./core/runner.js";
|
|
40
42
|
export { ExtensionRegistry } from "./extensions/registry.js";
|
|
41
43
|
export * from "./goal/index.js";
|
package/dist/plugins/host.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AskQuestionsRequest, AskQuestionsResult, CommandContext, CommandOptions, CompletionRequest, Message, PanelProvider, Plugin, PluginAPI, PluginMetadata, RunEvent, SelectRequest, ViewContext } from "@alisio/sdk";
|
|
2
2
|
import type { HookFailure, RunnerExtensions } from "../core/contracts.ts";
|
|
3
|
+
import type { HumanWaits } from "../core/human-wait.ts";
|
|
3
4
|
import type { ToolRegistry } from "../core/registry.ts";
|
|
4
5
|
import { ExtensionRegistry } from "../extensions/registry.ts";
|
|
5
6
|
import type { ProviderRegistry } from "../providers/registry.ts";
|
|
@@ -83,7 +84,15 @@ export declare class PluginHost implements RunnerExtensions {
|
|
|
83
84
|
private uiImpl?;
|
|
84
85
|
/** Binds the child session service once the runner exists. */
|
|
85
86
|
setSessions(sessions: PluginAPI["sessions"]): void;
|
|
86
|
-
/**
|
|
87
|
+
/** Where waits for a person are opened (see `HumanWaits`); bound once the runner exists. */
|
|
88
|
+
private humanWaits?;
|
|
89
|
+
setHumanWaits(waits: HumanWaits): void;
|
|
90
|
+
/**
|
|
91
|
+
* Binds interactive UI services (the TUI, the web server); without them select/askQuestions
|
|
92
|
+
* resolve undefined. Every question that reaches the person through here (plugins, the
|
|
93
|
+
* `ask_user_question` tool, the plan review) is a human wait: the bound UI is wrapped ONCE so no
|
|
94
|
+
* host and no caller can forget to mark it.
|
|
95
|
+
*/
|
|
87
96
|
setInteractiveUI(ui: {
|
|
88
97
|
select(request: SelectRequest): Promise<string | undefined>;
|
|
89
98
|
askQuestions(request: AskQuestionsRequest): Promise<AskQuestionsResult>;
|
package/dist/plugins/host.js
CHANGED
|
@@ -85,9 +85,24 @@ export class PluginHost {
|
|
|
85
85
|
setSessions(sessions) {
|
|
86
86
|
this.sessionsImpl = sessions;
|
|
87
87
|
}
|
|
88
|
-
/**
|
|
88
|
+
/** Where waits for a person are opened (see `HumanWaits`); bound once the runner exists. */
|
|
89
|
+
humanWaits;
|
|
90
|
+
setHumanWaits(waits) {
|
|
91
|
+
this.humanWaits = waits;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Binds interactive UI services (the TUI, the web server); without them select/askQuestions
|
|
95
|
+
* resolve undefined. Every question that reaches the person through here (plugins, the
|
|
96
|
+
* `ask_user_question` tool, the plan review) is a human wait: the bound UI is wrapped ONCE so no
|
|
97
|
+
* host and no caller can forget to mark it.
|
|
98
|
+
*/
|
|
89
99
|
setInteractiveUI(ui) {
|
|
90
|
-
this.
|
|
100
|
+
const wait = (session, ask) => this.humanWaits ? this.humanWaits.track(session, ask) : ask();
|
|
101
|
+
this.uiImpl = {
|
|
102
|
+
select: (request) => wait(undefined, () => ui.select(request)),
|
|
103
|
+
askQuestions: (request) => wait(request.session, () => ui.askQuestions(request)),
|
|
104
|
+
open: (sessionId) => ui.open(sessionId),
|
|
105
|
+
};
|
|
91
106
|
}
|
|
92
107
|
/**
|
|
93
108
|
* Read-only view of one plugin's shared state (written through `api.state.set`). The host layer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alisio/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Embeddable provider-agnostic Alisio agent core: tool loop, context compaction, local tools, MCP, plugin host and SQLite session store. Runs on Node >=22.16 and Bun.",
|
|
5
5
|
"author": "Gustavo Gutiérrez",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"node": ">=22.16"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@alisio/sdk": "0.2.
|
|
44
|
+
"@alisio/sdk": "0.2.1",
|
|
45
45
|
"@mixmark-io/domino": "2.2.0",
|
|
46
46
|
"@modelcontextprotocol/client": "2.1.0",
|
|
47
47
|
"ajv": "8.20.0",
|