@inhuman.tools/turn 0.2.0 → 0.3.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 +38 -0
- package/dist/agent-calls/acceptance.d.ts +1 -1
- package/dist/agent-calls/acceptance.d.ts.map +1 -1
- package/dist/agent-calls/acceptance.js +1 -1
- package/dist/agent-calls/acceptance.js.map +1 -1
- package/dist/agent-calls/callbacks.d.ts +1 -1
- package/dist/agent-calls/callbacks.d.ts.map +1 -1
- package/dist/agent-calls/callbacks.js +1 -1
- package/dist/agent-calls/callbacks.js.map +1 -1
- package/dist/agent-calls/delivery.d.ts +1 -1
- package/dist/agent-calls/delivery.d.ts.map +1 -1
- package/dist/agent-calls/delivery.js +1 -1
- package/dist/agent-calls/delivery.js.map +1 -1
- package/dist/agent-calls/operator.d.ts +1 -1
- package/dist/agent-calls/operator.d.ts.map +1 -1
- package/dist/agent-calls/operator.js +1 -1
- package/dist/agent-calls/operator.js.map +1 -1
- package/dist/agent-calls/runtime.d.ts +4 -2
- package/dist/agent-calls/runtime.d.ts.map +1 -1
- package/dist/agent-calls/runtime.js +17 -11
- package/dist/agent-calls/runtime.js.map +1 -1
- package/dist/agent-calls/types.d.ts +1 -1
- package/dist/agent-calls/types.d.ts.map +1 -1
- package/dist/context/compact-floors.d.ts +1 -1
- package/dist/context/compact-floors.d.ts.map +1 -1
- package/dist/context/compact.d.ts +1 -1
- package/dist/context/compact.d.ts.map +1 -1
- package/dist/context/compact.js +5 -4
- package/dist/context/compact.js.map +1 -1
- package/dist/context/history.d.ts +1 -1
- package/dist/context/history.d.ts.map +1 -1
- package/dist/context/history.js +35 -3
- package/dist/context/history.js.map +1 -1
- package/dist/context/retention/defaults.js +1 -1
- package/dist/context/retention/defaults.js.map +1 -1
- package/dist/context/retention/resolve.js +1 -1
- package/dist/context/retention/resolve.js.map +1 -1
- package/dist/context/retention/schema.js +1 -1
- package/dist/context/retention/schema.js.map +1 -1
- package/dist/context/retention/stamp.d.ts +1 -1
- package/dist/context/retention/stamp.d.ts.map +1 -1
- package/dist/context/retention/stamp.js +5 -2
- package/dist/context/retention/stamp.js.map +1 -1
- package/dist/context/retention/step.d.ts +1 -1
- package/dist/context/retention/step.d.ts.map +1 -1
- package/dist/context/retention/step.js +7 -4
- package/dist/context/retention/step.js.map +1 -1
- package/dist/context/retention/view.d.ts +1 -1
- package/dist/context/retention/view.d.ts.map +1 -1
- package/dist/context/retention/view.js +1 -1
- package/dist/context/retention/view.js.map +1 -1
- package/dist/context/spawn-history.d.ts +13 -0
- package/dist/context/spawn-history.d.ts.map +1 -0
- package/dist/context/spawn-history.js +82 -0
- package/dist/context/spawn-history.js.map +1 -0
- package/dist/context/squeeze.d.ts +1 -1
- package/dist/context/squeeze.d.ts.map +1 -1
- package/dist/context/squeeze.js +1 -1
- package/dist/context/squeeze.js.map +1 -1
- package/dist/context/tools.d.ts +2 -2
- package/dist/context/tools.d.ts.map +1 -1
- package/dist/context/tools.js +2 -2
- package/dist/context/tools.js.map +1 -1
- package/dist/context/transcript.d.ts +3 -1
- package/dist/context/transcript.d.ts.map +1 -1
- package/dist/context/transcript.js +17 -4
- package/dist/context/transcript.js.map +1 -1
- package/dist/policy-processor/classifier.d.ts.map +1 -1
- package/dist/policy-processor/classifier.js +3 -1
- package/dist/policy-processor/classifier.js.map +1 -1
- package/dist/policy-processor/scope.d.ts +1 -0
- package/dist/policy-processor/scope.d.ts.map +1 -1
- package/dist/policy-processor/scope.js +9 -2
- package/dist/policy-processor/scope.js.map +1 -1
- package/dist/policy-processor/unattended-guard.js +8 -2
- package/dist/policy-processor/unattended-guard.js.map +1 -1
- package/dist/resources/compact-assets.d.ts +5 -0
- package/dist/resources/compact-assets.d.ts.map +1 -0
- package/dist/resources/compact-assets.js +6 -0
- package/dist/resources/compact-assets.js.map +1 -0
- package/dist/resources/compact-snapshot.d.ts +21 -0
- package/dist/resources/compact-snapshot.d.ts.map +1 -0
- package/dist/resources/compact-snapshot.js +44 -0
- package/dist/resources/compact-snapshot.js.map +1 -0
- package/dist/resources/index.d.ts +6 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +19 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/module-bundle.d.ts +16 -0
- package/dist/resources/module-bundle.d.ts.map +1 -0
- package/dist/resources/module-bundle.js +78 -0
- package/dist/resources/module-bundle.js.map +1 -0
- package/dist/runtime/ambient-compact.d.ts +38 -0
- package/dist/runtime/ambient-compact.d.ts.map +1 -0
- package/dist/runtime/ambient-compact.js +123 -0
- package/dist/runtime/ambient-compact.js.map +1 -0
- package/dist/runtime/index.d.ts +9 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/ports.d.ts +33 -0
- package/dist/runtime/ports.d.ts.map +1 -0
- package/dist/runtime/ports.js +2 -0
- package/dist/runtime/ports.js.map +1 -0
- package/dist/runtime/prompt-message-of.d.ts +4 -0
- package/dist/runtime/prompt-message-of.d.ts.map +1 -0
- package/dist/runtime/prompt-message-of.js +8 -0
- package/dist/runtime/prompt-message-of.js.map +1 -0
- package/dist/runtime/re-act-loop-intent-detected-trigger.d.ts +32 -0
- package/dist/runtime/re-act-loop-intent-detected-trigger.d.ts.map +1 -0
- package/dist/runtime/re-act-loop-intent-detected-trigger.js +132 -0
- package/dist/runtime/re-act-loop-intent-detected-trigger.js.map +1 -0
- package/dist/runtime/round-boundary.d.ts +13 -0
- package/dist/runtime/round-boundary.d.ts.map +1 -0
- package/dist/runtime/round-boundary.js +210 -0
- package/dist/runtime/round-boundary.js.map +1 -0
- package/dist/runtime/round-stop.d.ts +9 -0
- package/dist/runtime/round-stop.d.ts.map +1 -0
- package/dist/runtime/round-stop.js +19 -0
- package/dist/runtime/round-stop.js.map +1 -0
- package/dist/runtime/tool-call.d.ts +4 -0
- package/dist/runtime/tool-call.d.ts.map +1 -0
- package/dist/runtime/tool-call.js +8 -0
- package/dist/runtime/tool-call.js.map +1 -0
- package/dist/runtime/turn-iterator.d.ts +32 -0
- package/dist/runtime/turn-iterator.d.ts.map +1 -0
- package/dist/runtime/turn-iterator.js +251 -0
- package/dist/runtime/turn-iterator.js.map +1 -0
- package/dist/runtime/turn-resolvers.d.ts +17 -0
- package/dist/runtime/turn-resolvers.d.ts.map +1 -0
- package/dist/runtime/turn-resolvers.js +29 -0
- package/dist/runtime/turn-resolvers.js.map +1 -0
- package/dist/session/cancel.d.ts +4 -1
- package/dist/session/cancel.d.ts.map +1 -1
- package/dist/session/cancel.js +10 -2
- package/dist/session/cancel.js.map +1 -1
- package/dist/session/commands.d.ts +1 -1
- package/dist/session/commands.d.ts.map +1 -1
- package/dist/session/commands.js +4 -2
- package/dist/session/commands.js.map +1 -1
- package/dist/session/compact-actor.d.ts +12 -5
- package/dist/session/compact-actor.d.ts.map +1 -1
- package/dist/session/compact-actor.js +64 -12
- package/dist/session/compact-actor.js.map +1 -1
- package/dist/session/compact-env.d.ts +31 -5
- package/dist/session/compact-env.d.ts.map +1 -1
- package/dist/session/compact-env.js +23 -13
- package/dist/session/compact-env.js.map +1 -1
- package/dist/session/compact.d.ts +6 -3
- package/dist/session/compact.d.ts.map +1 -1
- package/dist/session/compact.js.map +1 -1
- package/dist/session/index.d.ts +2 -2
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +2 -2
- package/dist/session/index.js.map +1 -1
- package/dist/session/park.d.ts +1 -1
- package/dist/session/park.d.ts.map +1 -1
- package/dist/session/park.js +1 -1
- package/dist/session/park.js.map +1 -1
- package/dist/session/side-channel-reminder.d.ts +1 -1
- package/dist/session/side-channel-reminder.d.ts.map +1 -1
- package/dist/session/side-channel-reminder.js +1 -1
- package/dist/session/side-channel-reminder.js.map +1 -1
- package/dist/session/types.d.ts +1 -1
- package/dist/session/types.d.ts.map +1 -1
- package/dist/session-runtime/ask-line.d.ts +7 -0
- package/dist/session-runtime/ask-line.d.ts.map +1 -0
- package/dist/session-runtime/ask-line.js +36 -0
- package/dist/session-runtime/ask-line.js.map +1 -0
- package/dist/session-runtime/durability.d.ts +25 -0
- package/dist/session-runtime/durability.d.ts.map +1 -0
- package/dist/session-runtime/durability.js +18 -0
- package/dist/session-runtime/durability.js.map +1 -0
- package/dist/session-runtime/effects.d.ts +53 -0
- package/dist/session-runtime/effects.d.ts.map +1 -0
- package/dist/session-runtime/effects.js +18 -0
- package/dist/session-runtime/effects.js.map +1 -0
- package/dist/session-runtime/index.d.ts +8 -0
- package/dist/session-runtime/index.d.ts.map +1 -0
- package/dist/session-runtime/index.js +8 -0
- package/dist/session-runtime/index.js.map +1 -0
- package/dist/session-runtime/interrupt.d.ts +3 -0
- package/dist/session-runtime/interrupt.d.ts.map +1 -0
- package/dist/session-runtime/interrupt.js +33 -0
- package/dist/session-runtime/interrupt.js.map +1 -0
- package/dist/session-runtime/recovery.d.ts +17 -0
- package/dist/session-runtime/recovery.d.ts.map +1 -0
- package/dist/session-runtime/recovery.js +119 -0
- package/dist/session-runtime/recovery.js.map +1 -0
- package/dist/session-runtime/runtime.d.ts +50 -0
- package/dist/session-runtime/runtime.d.ts.map +1 -0
- package/dist/session-runtime/runtime.js +533 -0
- package/dist/session-runtime/runtime.js.map +1 -0
- package/dist/session-runtime/summary.d.ts +26 -0
- package/dist/session-runtime/summary.d.ts.map +1 -0
- package/dist/session-runtime/summary.js +33 -0
- package/dist/session-runtime/summary.js.map +1 -0
- package/dist/tool-catalog/bindings.d.ts +6 -0
- package/dist/tool-catalog/bindings.d.ts.map +1 -0
- package/dist/tool-catalog/bindings.js +37 -0
- package/dist/tool-catalog/bindings.js.map +1 -0
- package/dist/tool-catalog/catalog.d.ts +1 -3
- package/dist/tool-catalog/catalog.d.ts.map +1 -1
- package/dist/tool-catalog/catalog.js +6 -34
- package/dist/tool-catalog/catalog.js.map +1 -1
- package/dist/tool-catalog/types.d.ts +8 -0
- package/dist/tool-catalog/types.d.ts.map +1 -1
- package/dist/tool-catalog/types.js.map +1 -1
- package/dist/tool-runtime/approval/approved.d.ts +1 -1
- package/dist/tool-runtime/approval/approved.d.ts.map +1 -1
- package/dist/tool-runtime/approval/approved.js +1 -1
- package/dist/tool-runtime/approval/approved.js.map +1 -1
- package/dist/tool-runtime/handler.d.ts.map +1 -1
- package/dist/tool-runtime/handler.js +291 -237
- package/dist/tool-runtime/handler.js.map +1 -1
- package/dist/tool-runtime/run-system-tool.d.ts +1 -0
- package/dist/tool-runtime/run-system-tool.d.ts.map +1 -1
- package/dist/tool-runtime/run-system-tool.js +30 -10
- package/dist/tool-runtime/run-system-tool.js.map +1 -1
- package/dist/tool-runtime/worker-tools.d.ts.map +1 -1
- package/dist/tool-runtime/worker-tools.js +2 -2
- package/dist/tool-runtime/worker-tools.js.map +1 -1
- package/package.json +29 -16
- package/src/agent-calls/acceptance.ts +1 -1
- package/src/agent-calls/callbacks.ts +1 -1
- package/src/agent-calls/delivery.ts +1 -1
- package/src/agent-calls/operator.ts +7 -1
- package/src/agent-calls/runtime.ts +23 -11
- package/src/agent-calls/types.ts +1 -1
- package/src/context/compact-floors.ts +1 -1
- package/src/context/compact.ts +4 -5
- package/src/context/history.ts +46 -4
- package/src/context/retention/defaults.ts +1 -1
- package/src/context/retention/resolve.ts +1 -1
- package/src/context/retention/schema.ts +1 -1
- package/src/context/retention/stamp.ts +7 -2
- package/src/context/retention/step.ts +7 -4
- package/src/context/retention/view.ts +1 -1
- package/src/context/spawn-history.ts +95 -0
- package/src/context/squeeze.ts +1 -1
- package/src/context/tools.ts +3 -2
- package/src/context/transcript.ts +15 -4
- package/src/policy-processor/README.md +1 -1
- package/src/policy-processor/classifier.ts +3 -1
- package/src/policy-processor/scope.ts +7 -2
- package/src/policy-processor/unattended-guard.ts +5 -2
- package/src/resources/compact-assets.ts +9 -0
- package/src/resources/compact-snapshot.ts +51 -0
- package/src/resources/index.ts +27 -0
- package/src/resources/module-bundle.ts +90 -0
- package/src/runtime/ambient-compact.ts +147 -0
- package/src/runtime/index.ts +17 -0
- package/src/runtime/ports.ts +28 -0
- package/src/runtime/prompt-message-of.ts +14 -0
- package/src/runtime/re-act-loop-intent-detected-trigger.ts +177 -0
- package/src/runtime/round-boundary.ts +248 -0
- package/src/runtime/round-stop.ts +24 -0
- package/src/runtime/tool-call.ts +10 -0
- package/src/runtime/turn-iterator.ts +312 -0
- package/src/runtime/turn-resolvers.ts +43 -0
- package/src/session/cancel.ts +11 -3
- package/src/session/commands.ts +5 -2
- package/src/session/compact-actor.ts +76 -16
- package/src/session/compact-env.ts +27 -18
- package/src/session/compact.ts +8 -3
- package/src/session/index.ts +7 -2
- package/src/session/park.ts +7 -1
- package/src/session/side-channel-reminder.ts +1 -1
- package/src/session/types.ts +1 -1
- package/src/session-runtime/ask-line.ts +39 -0
- package/src/session-runtime/durability.ts +41 -0
- package/src/session-runtime/effects.ts +35 -0
- package/src/session-runtime/index.ts +19 -0
- package/src/session-runtime/interrupt.ts +34 -0
- package/src/session-runtime/recovery.ts +145 -0
- package/src/session-runtime/runtime.ts +535 -0
- package/src/session-runtime/summary.ts +67 -0
- package/src/tool-catalog/bindings.ts +37 -0
- package/src/tool-catalog/catalog.ts +6 -31
- package/src/tool-catalog/types.ts +9 -0
- package/src/tool-runtime/approval/approved.ts +1 -1
- package/src/tool-runtime/handler.ts +40 -24
- package/src/tool-runtime/run-system-tool.ts +27 -11
- package/src/tool-runtime/worker-tools.ts +2 -2
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/** Detect positive continuation requests from settled facts; never infer completion from silence. */
|
|
2
|
+
import {
|
|
3
|
+
RE_ACT_LOOP_INTENT_DETECTED_TRIGGER_STREAK_MAX,
|
|
4
|
+
THOUGHT_WITHOUT_ANSWER_STREAK_MAX,
|
|
5
|
+
} from "@inhuman.tools/continuation";
|
|
6
|
+
import { asSystemPrompt, prompt } from "@inhuman.tools/harness-commons";
|
|
7
|
+
import {
|
|
8
|
+
callbackRequiredNote,
|
|
9
|
+
decideRound,
|
|
10
|
+
ReACTChunk,
|
|
11
|
+
SessionTurn,
|
|
12
|
+
TurnSteerChunk,
|
|
13
|
+
jobTriggersReactLoop,
|
|
14
|
+
jobContinuationOf,
|
|
15
|
+
type CompletionJob,
|
|
16
|
+
type ContinuationCandidate,
|
|
17
|
+
type ContinuationKind,
|
|
18
|
+
type IntentAssessment,
|
|
19
|
+
type RoundDecision,
|
|
20
|
+
type RoundGate,
|
|
21
|
+
type SessionRoot,
|
|
22
|
+
} from "@inhuman.tools/harness-model/session";
|
|
23
|
+
import { splitThink } from "@inhuman.tools/tool-call-normalize";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
LENGTH_CONTINUE_NOTE,
|
|
27
|
+
RE_ACT_LOOP_INTENT_DETECTED_TRIGGER_NOTE,
|
|
28
|
+
THOUGHT_CONTINUE_NOTE,
|
|
29
|
+
thoughtWithoutAnswer,
|
|
30
|
+
} from "./re-act-loop-intent-detected-trigger.js";
|
|
31
|
+
import { CONTENT_FILTER_STOP, maxRoundsMessage, roundCapReached } from "./round-stop.js";
|
|
32
|
+
import type { TurnIteratorHost } from "./turn-iterator.js";
|
|
33
|
+
import { joinSystemNotes } from "../session/side-channel-reminder.js";
|
|
34
|
+
|
|
35
|
+
const REPAIR_ATTEMPTS = 4;
|
|
36
|
+
const THOUGHT_ATTEMPTS = THOUGHT_WITHOUT_ANSWER_STREAK_MAX;
|
|
37
|
+
const INTENT_ATTEMPTS = RE_ACT_LOOP_INTENT_DETECTED_TRIGGER_STREAK_MAX;
|
|
38
|
+
|
|
39
|
+
/** Only an actual successful work receipt resets repair budgets. Rejected calls are not progress. */
|
|
40
|
+
function madeProgress(job: CompletionJob): boolean {
|
|
41
|
+
return job.responseWork.some((work) => jobTriggersReactLoop(work) && work.outcomeSnap?.ok === true);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function attempts(turn: SessionTurn, boundary: ReACTChunk, kind: ContinuationKind): number {
|
|
45
|
+
if (boundary.source != null && madeProgress(boundary.source)) return 0;
|
|
46
|
+
let count = 0;
|
|
47
|
+
for (let i = turn.doneWork.length - 1; i >= 0; i -= 1) {
|
|
48
|
+
const cell = turn.doneWork[i];
|
|
49
|
+
if (cell === boundary) continue;
|
|
50
|
+
if (cell instanceof TurnSteerChunk) break;
|
|
51
|
+
if (!(cell instanceof ReACTChunk)) continue;
|
|
52
|
+
const decision = cell.decision;
|
|
53
|
+
if (
|
|
54
|
+
decision?.kind === "advance" &&
|
|
55
|
+
decision.assessments.some(
|
|
56
|
+
(item) => item.cause.kind === kind && item.disposition === "eligible" && item.budget != null,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
count += 1;
|
|
60
|
+
if (cell.source != null && madeProgress(cell.source)) break;
|
|
61
|
+
}
|
|
62
|
+
return count;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function openCallbackRowsForTurn(session: SessionRoot, turn: SessionTurn) {
|
|
66
|
+
const future = new Set<string>();
|
|
67
|
+
const seen = new Set<SessionTurn>();
|
|
68
|
+
let unit = session.head;
|
|
69
|
+
while (unit != null && unit !== turn && !seen.has(unit)) {
|
|
70
|
+
seen.add(unit);
|
|
71
|
+
if (unit.agentRequestId.trim() !== "") future.add(unit.agentRequestId.trim());
|
|
72
|
+
if (!(unit.turnTail instanceof SessionTurn)) break;
|
|
73
|
+
unit = unit.turnTail;
|
|
74
|
+
}
|
|
75
|
+
return session.openCallbackRows().filter((row) => !future.has(row.requestId));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function liveCandidates(turn: SessionTurn, host: TurnIteratorHost): ContinuationCandidate[] {
|
|
79
|
+
const out: ContinuationCandidate[] = [];
|
|
80
|
+
const session = host.session;
|
|
81
|
+
const input = [
|
|
82
|
+
...(session.steerTurn === turn ? session.steerQueue.map((row) => row.uuid) : []),
|
|
83
|
+
...session.pendingNotices.map((note) => note.uuid),
|
|
84
|
+
];
|
|
85
|
+
if (input.length > 0) out.push({ cause: { kind: "input", sources: input } });
|
|
86
|
+
const activating = host.notes
|
|
87
|
+
.peek(session.sessionId)
|
|
88
|
+
.filter((note) => note.mode === "activating" && note.text.trim() !== "");
|
|
89
|
+
if (activating.length > 0) out.push({ cause: { kind: "plugin", sources: activating.map((note) => note.id) } });
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** No effects except the bounded intent assessment. The caller commits the returned decision. */
|
|
94
|
+
export async function assessBoundary(
|
|
95
|
+
turn: SessionTurn,
|
|
96
|
+
boundary: ReACTChunk,
|
|
97
|
+
host: TurnIteratorHost,
|
|
98
|
+
signal: AbortSignal,
|
|
99
|
+
): Promise<RoundDecision> {
|
|
100
|
+
const source = boundary.source;
|
|
101
|
+
const pew = source?.expectation;
|
|
102
|
+
const candidates: ContinuationCandidate[] = [];
|
|
103
|
+
let gate: RoundGate | null = null;
|
|
104
|
+
const add = (kind: ContinuationKind, sources: readonly string[], limit?: number): void => {
|
|
105
|
+
candidates.push({
|
|
106
|
+
cause: { kind, sources },
|
|
107
|
+
...(limit == null ? {} : { budget: { used: attempts(turn, boundary, kind), limit } }),
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
if (source == null) {
|
|
111
|
+
// Old documents had executable tokens with no recorded cause. Interpret only at this compatibility boundary.
|
|
112
|
+
if (!boundary.openPage)
|
|
113
|
+
add(boundary.truncated ? "length-repair" : boundary.intentContinue ? "intent" : "user", [boundary.uuid]);
|
|
114
|
+
} else if (pew != null && pew.outcome !== "successful") {
|
|
115
|
+
gate = {
|
|
116
|
+
reason: "provider-failed",
|
|
117
|
+
detail: `model round failed: ${pew.failReason?.trim() || pew.endDetail?.trim() || "no reason given"}`,
|
|
118
|
+
};
|
|
119
|
+
} else if (pew != null) {
|
|
120
|
+
const rejected = new Set(pew.responseCalls.filter((call) => call.dropReason !== "").map((call) => call.toolCallId));
|
|
121
|
+
const admitted = source.responseWork.filter((job) => !rejected.has(job.toolCallId));
|
|
122
|
+
const work = admitted.filter((job) => jobContinuationOf(job).kind === "observation");
|
|
123
|
+
const auxiliary = admitted.filter((job) => jobContinuationOf(job).kind === "auxiliary");
|
|
124
|
+
const speech = splitThink(pew.responseText).content.trim();
|
|
125
|
+
if (pew.finishReason === CONTENT_FILTER_STOP) {
|
|
126
|
+
gate = {
|
|
127
|
+
reason: "content-filter",
|
|
128
|
+
detail: "The provider's content filter stopped this response. Send another message to try again.",
|
|
129
|
+
};
|
|
130
|
+
} else {
|
|
131
|
+
if (work.length > 0)
|
|
132
|
+
add(
|
|
133
|
+
"observation",
|
|
134
|
+
work.map((job) => job.uuid),
|
|
135
|
+
madeProgress(source) ? undefined : REPAIR_ATTEMPTS,
|
|
136
|
+
);
|
|
137
|
+
if (rejected.size > 0) add("call-repair", [...rejected], REPAIR_ATTEMPTS);
|
|
138
|
+
if (pew.finishReason === "length" && work.length === 0 && rejected.size === 0) {
|
|
139
|
+
if (speech === "")
|
|
140
|
+
gate = {
|
|
141
|
+
reason: "length-no-answer",
|
|
142
|
+
detail:
|
|
143
|
+
"The model used up its output budget while reasoning and gave no answer. Raise the output limit or ask again more narrowly.",
|
|
144
|
+
};
|
|
145
|
+
else add("length-repair", [source.uuid], turn.lengthContinuations ?? 3);
|
|
146
|
+
} else if (work.length === 0 && rejected.size === 0) {
|
|
147
|
+
if (thoughtWithoutAnswer(pew)) add("thought-repair", [source.uuid], THOUGHT_ATTEMPTS);
|
|
148
|
+
else if (speech === "" && auxiliary.length > 0) {
|
|
149
|
+
add(
|
|
150
|
+
"auxiliary-repair",
|
|
151
|
+
auxiliary.map((job) => job.uuid),
|
|
152
|
+
REPAIR_ATTEMPTS,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const callbacks = openCallbackRowsForTurn(host.session, turn);
|
|
157
|
+
if (callbacks.length > 0)
|
|
158
|
+
add(
|
|
159
|
+
"callback",
|
|
160
|
+
callbacks.map((row) => row.requestId),
|
|
161
|
+
REPAIR_ATTEMPTS,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
candidates.push(...liveCandidates(turn, host));
|
|
166
|
+
// A judge is needed only when no cheaper trigger already authorizes the next invocation.
|
|
167
|
+
const preliminary = decideRound(candidates, gate);
|
|
168
|
+
if (
|
|
169
|
+
pew != null &&
|
|
170
|
+
gate == null &&
|
|
171
|
+
preliminary.kind !== "advance" &&
|
|
172
|
+
splitThink(pew.responseText).content.trim() !== ""
|
|
173
|
+
) {
|
|
174
|
+
if (boundary.intentAssessment == null) {
|
|
175
|
+
let assessment: IntentAssessment;
|
|
176
|
+
try {
|
|
177
|
+
assessment = await host.classifyIntent(pew, signal);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
assessment = {
|
|
180
|
+
verdict: null,
|
|
181
|
+
status: "failed",
|
|
182
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (!signal.aborted) boundary.intentAssessment = assessment;
|
|
186
|
+
}
|
|
187
|
+
if (boundary.intentAssessment?.verdict === "continue") add("intent", [source!.uuid], INTENT_ATTEMPTS);
|
|
188
|
+
}
|
|
189
|
+
// Input may arrive while the judge runs. Refresh event facts before the synchronous commit.
|
|
190
|
+
const stable = candidates.filter(
|
|
191
|
+
(candidate) => candidate.cause.kind !== "input" && candidate.cause.kind !== "plugin",
|
|
192
|
+
);
|
|
193
|
+
const finalCandidates = [...stable, ...liveCandidates(turn, host)];
|
|
194
|
+
if (gate == null && roundCapReached(turn) && decideRound(finalCandidates).kind === "advance") {
|
|
195
|
+
gate = { reason: "round-limit", detail: maxRoundsMessage(turn.maxRounds ?? 0) };
|
|
196
|
+
}
|
|
197
|
+
return decideRound(finalCandidates, gate, boundary.intentAssessment);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Every admitted cause contributes context. Ambient notes enrich a round but cannot authorize one. */
|
|
201
|
+
export function boundaryNote(turn: SessionTurn, boundary: ReACTChunk | null, host: TurnIteratorHost) {
|
|
202
|
+
const kinds = new Set(boundary?.decision?.causes.map((cause) => cause.kind));
|
|
203
|
+
const notes = [];
|
|
204
|
+
if (kinds.has("length-repair")) notes.push(LENGTH_CONTINUE_NOTE);
|
|
205
|
+
if (kinds.has("thought-repair")) notes.push(THOUGHT_CONTINUE_NOTE);
|
|
206
|
+
if (kinds.has("intent")) notes.push(RE_ACT_LOOP_INTENT_DETECTED_TRIGGER_NOTE);
|
|
207
|
+
if (kinds.has("auxiliary-repair") && boundary?.source != null) {
|
|
208
|
+
const sources = new Set(
|
|
209
|
+
boundary.decision?.causes.filter((cause) => cause.kind === "auxiliary-repair").flatMap((cause) => cause.sources),
|
|
210
|
+
);
|
|
211
|
+
// Older committed decisions named the completion, rather than its auxiliary jobs.
|
|
212
|
+
const legacy = sources.has(boundary.source.uuid);
|
|
213
|
+
const auxiliaryNotes = new Set<string>();
|
|
214
|
+
for (const job of boundary.source.responseWork) {
|
|
215
|
+
if (!legacy && !sources.has(job.uuid)) continue;
|
|
216
|
+
const policy = jobContinuationOf(job);
|
|
217
|
+
if (policy.kind === "auxiliary" && policy.note != null) auxiliaryNotes.add(policy.note);
|
|
218
|
+
}
|
|
219
|
+
for (const note of auxiliaryNotes) notes.push(asSystemPrompt(note));
|
|
220
|
+
}
|
|
221
|
+
const callbacks = openCallbackRowsForTurn(host.session, turn);
|
|
222
|
+
if (callbacks.length > 0) notes.push(callbackRequiredNote(host.session.agent.spec.voidLegal, callbacks));
|
|
223
|
+
const pending = host.notes.peek(host.session.sessionId).filter((note) => note.text.trim() !== "");
|
|
224
|
+
const recorded = boundary?.contextNotes ?? [];
|
|
225
|
+
const context = [
|
|
226
|
+
...recorded,
|
|
227
|
+
...pending.filter((note) => !recorded.some((old) => old.id === note.id && old.text === note.text)),
|
|
228
|
+
];
|
|
229
|
+
if (context.length > 0) notes.push(asSystemPrompt(context.map((note) => note.text).join("\n\n")));
|
|
230
|
+
return { note: notes.length > 0 ? joinSystemNotes(notes) : prompt.system``, noteIds: pending.map((note) => note.id) };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Re-read inbox facts at the commit site: an awaited detector must not seal over new input. */
|
|
234
|
+
export function refreshBoundaryEvents(
|
|
235
|
+
turn: SessionTurn,
|
|
236
|
+
host: TurnIteratorHost,
|
|
237
|
+
decision: RoundDecision,
|
|
238
|
+
): RoundDecision {
|
|
239
|
+
const candidates = decision.assessments.filter((item) => item.cause.kind !== "input" && item.cause.kind !== "plugin");
|
|
240
|
+
const live = [...candidates, ...liveCandidates(turn, host)];
|
|
241
|
+
const gate =
|
|
242
|
+
decision.kind === "blocked"
|
|
243
|
+
? { reason: decision.reason, detail: decision.detail }
|
|
244
|
+
: roundCapReached(turn) && decideRound(live).kind === "advance"
|
|
245
|
+
? { reason: "round-limit" as const, detail: maxRoundsMessage(turn.maxRounds ?? 0) }
|
|
246
|
+
: null;
|
|
247
|
+
return decideRound(live, gate, decision.intent);
|
|
248
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Provider facts and admission limits. Continuation policy lives in round-boundary. */
|
|
2
|
+
import type { SessionTurn } from "@inhuman.tools/harness-model/session";
|
|
3
|
+
|
|
4
|
+
export const CONTENT_FILTER_STOP = "content_filter";
|
|
5
|
+
export const LENGTH_STOP = "length";
|
|
6
|
+
|
|
7
|
+
/** The receipt for a call that was not run. */
|
|
8
|
+
export function refusalText(dropReason: string, filtered: boolean): string {
|
|
9
|
+
if (dropReason === "truncated") {
|
|
10
|
+
return "Your tool call was truncated (the response hit its output limit mid-call), so it was not run. Re-issue it.";
|
|
11
|
+
}
|
|
12
|
+
if (dropReason !== "") {
|
|
13
|
+
return "Your tool call's arguments could not be parsed, so it was not run. Re-issue it with valid arguments.";
|
|
14
|
+
}
|
|
15
|
+
return filtered ? "Not run: the provider's content filter stopped this response." : "Not run.";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function maxRoundsMessage(limit: number): string {
|
|
19
|
+
return `Stopped after ${limit} model rounds: the round limit for this turn (maxRounds) is reached. Send another message to continue.`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function roundCapReached(turn: SessionTurn): boolean {
|
|
23
|
+
return turn.maxRounds != null && turn.maxRounds > 0 && turn.rounds >= turn.maxRounds;
|
|
24
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ResponseCall } from "@inhuman.tools/harness-model/session";
|
|
2
|
+
import type { ToolCall } from "@inhuman.tools/tool-call-normalize";
|
|
3
|
+
|
|
4
|
+
export function toolCallFromResponse(call: ResponseCall): ToolCall {
|
|
5
|
+
return {
|
|
6
|
+
id: call.toolCallId,
|
|
7
|
+
type: "function",
|
|
8
|
+
function: { name: call.name, arguments: JSON.stringify(call.args ?? {}) },
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { prompt } from "@inhuman.tools/harness-commons";
|
|
2
|
+
import {
|
|
3
|
+
CompactJob,
|
|
4
|
+
CompletionExpectation,
|
|
5
|
+
CompletionJob,
|
|
6
|
+
DEFAULT_LENGTH_CONTINUATIONS,
|
|
7
|
+
ResponseCallOrder,
|
|
8
|
+
SystemToolJob,
|
|
9
|
+
parseCompactTrigger,
|
|
10
|
+
ProgrammaticJob,
|
|
11
|
+
ReACTChunk,
|
|
12
|
+
SessionRoot,
|
|
13
|
+
SessionTurn,
|
|
14
|
+
ToolCallJob,
|
|
15
|
+
TurnPromptChunk,
|
|
16
|
+
type TurnChunk,
|
|
17
|
+
type IntentAssessment,
|
|
18
|
+
type RoundGate,
|
|
19
|
+
} from "@inhuman.tools/harness-model/session";
|
|
20
|
+
import { type ToolCall } from "@inhuman.tools/tool-call-normalize";
|
|
21
|
+
import { runInAction } from "mobx";
|
|
22
|
+
|
|
23
|
+
import { ambientCutoff, ambientModel, type startAmbientCompact } from "./ambient-compact.js";
|
|
24
|
+
import type { RuntimeDiagnostics, RuntimeNoteLog, TurnSettle } from "./ports.js";
|
|
25
|
+
import { questionLineFromSpeech } from "./re-act-loop-intent-detected-trigger.js";
|
|
26
|
+
import { assessBoundary, boundaryNote, refreshBoundaryEvents } from "./round-boundary.js";
|
|
27
|
+
import { CONTENT_FILTER_STOP, maxRoundsMessage, refusalText, roundCapReached } from "./round-stop.js";
|
|
28
|
+
import { toolCallFromResponse } from "./tool-call.js";
|
|
29
|
+
import type { TurnResolvers } from "./turn-resolvers.js";
|
|
30
|
+
import {
|
|
31
|
+
compactDecision,
|
|
32
|
+
historyBehind,
|
|
33
|
+
stampRetentionClock,
|
|
34
|
+
keepFloors,
|
|
35
|
+
stampCompactInputs,
|
|
36
|
+
sliceCompactZones,
|
|
37
|
+
tailCompactCandidate,
|
|
38
|
+
Transcript,
|
|
39
|
+
} from "../context/index.js";
|
|
40
|
+
import { isWorkReachable, turnLoopStopped } from "../session/cancel.js";
|
|
41
|
+
import {
|
|
42
|
+
joinSystemNotes,
|
|
43
|
+
sealedActorRounds,
|
|
44
|
+
SIDE_CHANNEL_REMINDER_EVERY,
|
|
45
|
+
sideChannelReminderNote,
|
|
46
|
+
} from "../session/side-channel-reminder.js";
|
|
47
|
+
import { catalogOf } from "../tool-catalog/bindings.js";
|
|
48
|
+
import { behaviorEntriesOf } from "../tool-catalog/catalog.js";
|
|
49
|
+
|
|
50
|
+
/** Sequence rules depend on document data and explicit ports, never a daemon. */
|
|
51
|
+
export interface TurnIteratorHost {
|
|
52
|
+
readonly session: SessionRoot;
|
|
53
|
+
readonly resolvers: TurnResolvers;
|
|
54
|
+
readonly notes: RuntimeNoteLog;
|
|
55
|
+
readonly diagnostics?: RuntimeDiagnostics;
|
|
56
|
+
readonly compactKeepTurns: number;
|
|
57
|
+
readonly hardCompactTrigger: string;
|
|
58
|
+
admit(call: ToolCall): ToolCallJob | ProgrammaticJob;
|
|
59
|
+
waitInbound(signal: AbortSignal): Promise<boolean>;
|
|
60
|
+
openRound(resumed: boolean): Promise<boolean>;
|
|
61
|
+
closeRound(settle: TurnSettle): Promise<void>;
|
|
62
|
+
classifyIntent(completion: CompletionExpectation, signal: AbortSignal): Promise<IntentAssessment>;
|
|
63
|
+
startAmbientCompact(options: Omit<Parameters<typeof startAmbientCompact>[0], "complete" | "inflight">): void;
|
|
64
|
+
/**
|
|
65
|
+
* The loop limits to record on this turn when it starts. Absent means unlimited rounds and
|
|
66
|
+
* the default number of length continuations.
|
|
67
|
+
*/
|
|
68
|
+
loopLimits?(turn: SessionTurn): { readonly maxRounds: number; readonly lengthContinuations: number };
|
|
69
|
+
/** True for a third-party MCP server. Absent, or an unknown connection, is local and runs in order. */
|
|
70
|
+
remoteMcp?(connectionKey: string): boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Append-and-iterate view of the persisted tape. The doneWork/queue
|
|
75
|
+
* split is the durable cursor; entering a cell is independent of resolving it.
|
|
76
|
+
* No snapshot iterator: appends are visited in this same pass.
|
|
77
|
+
*/
|
|
78
|
+
function* turnSequence(turn: SessionTurn, signal: AbortSignal): Generator<TurnChunk> {
|
|
79
|
+
while (!turnLoopStopped(turn, signal)) {
|
|
80
|
+
const cell = turn.take();
|
|
81
|
+
if (cell == null) return;
|
|
82
|
+
yield cell;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function iterateTurn(turn: SessionTurn, host: TurnIteratorHost, signal: AbortSignal): Promise<void> {
|
|
87
|
+
if (turnLoopStopped(turn, signal)) return;
|
|
88
|
+
const session = host.session;
|
|
89
|
+
const connectionId = session.kvs.connection?.trim() ?? "";
|
|
90
|
+
const model = session.kvs.model?.trim() ?? "";
|
|
91
|
+
// Recorded once: a live setting change does not move a turn that is already running.
|
|
92
|
+
turn.stampLoopLimits(host.loopLimits?.(turn) ?? { maxRounds: 0, lengthContinuations: DEFAULT_LENGTH_CONTINUATIONS });
|
|
93
|
+
for (const chunk of turnSequence(turn, signal)) {
|
|
94
|
+
if (turnLoopStopped(turn, signal)) return;
|
|
95
|
+
switch (true) {
|
|
96
|
+
case chunk instanceof CompactJob: {
|
|
97
|
+
await host.resolvers.compact(chunk, signal);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
case chunk instanceof TurnPromptChunk:
|
|
101
|
+
case chunk instanceof ReACTChunk: {
|
|
102
|
+
if (chunk instanceof ReACTChunk) {
|
|
103
|
+
if (chunk.decision == null) {
|
|
104
|
+
if (!chunk.roundClosed) {
|
|
105
|
+
const pew = chunk.source?.expectation;
|
|
106
|
+
await host.closeRound(
|
|
107
|
+
pew?.outcome === "failed"
|
|
108
|
+
? { ok: false, error: pew.failReason ?? pew.endDetail ?? "model round failed" }
|
|
109
|
+
: { ok: true, value: pew?.responseText || false },
|
|
110
|
+
);
|
|
111
|
+
if (turnLoopStopped(turn, signal)) return;
|
|
112
|
+
chunk.roundClosed = true;
|
|
113
|
+
}
|
|
114
|
+
const assessed = await assessBoundary(turn, chunk, host, signal);
|
|
115
|
+
if (turnLoopStopped(turn, signal)) return;
|
|
116
|
+
const decision = refreshBoundaryEvents(turn, host, assessed);
|
|
117
|
+
turn.commitBoundary(
|
|
118
|
+
chunk,
|
|
119
|
+
decision,
|
|
120
|
+
host.notes.peek(session.sessionId),
|
|
121
|
+
decision.reason === "awaiting-user" && chunk.source != null
|
|
122
|
+
? questionLineFromSpeech(chunk.source.expectation.responseText)
|
|
123
|
+
: "",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (chunk.decision?.kind !== "advance") continue;
|
|
127
|
+
if (chunk.decision.nextRoundId != null) continue;
|
|
128
|
+
}
|
|
129
|
+
if (turnLoopStopped(turn, signal)) return;
|
|
130
|
+
if (connectionId === "") {
|
|
131
|
+
blockAdmission(turn, chunk, {
|
|
132
|
+
reason: "no-connection",
|
|
133
|
+
detail: "no connection is configured for this session",
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (chunk instanceof ReACTChunk && !isWorkReachable(session, turn)) return;
|
|
138
|
+
if (roundCapReached(turn)) {
|
|
139
|
+
blockAdmission(turn, chunk, { reason: "round-limit", detail: maxRoundsMessage(turn.maxRounds ?? 0) });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (chunk instanceof ReACTChunk) chunk.waitingFor = "inbound";
|
|
143
|
+
if (!(await host.waitInbound(signal)) || turnLoopStopped(turn, signal)) return;
|
|
144
|
+
const resumed = turn.doneWork.some(
|
|
145
|
+
(done) => done !== chunk && (done instanceof TurnPromptChunk || done instanceof ReACTChunk),
|
|
146
|
+
);
|
|
147
|
+
if (chunk instanceof ReACTChunk) chunk.waitingFor = "round-hook";
|
|
148
|
+
const opened = await host.openRound(resumed);
|
|
149
|
+
if (turnLoopStopped(turn, signal)) return;
|
|
150
|
+
if (!opened) {
|
|
151
|
+
blockAdmission(turn, chunk, { reason: "plugin-refused", detail: "a plugin refused to open the round" });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
turn.claimApprovalNotes();
|
|
155
|
+
session.claimSteers(turn);
|
|
156
|
+
session.claimNotices(turn);
|
|
157
|
+
const messages = historyBehind(turn, compactRead(host.compactKeepTurns, session.kvs));
|
|
158
|
+
const completion = new CompletionExpectation({
|
|
159
|
+
promptText: chunk instanceof TurnPromptChunk ? turn.completionMessage : prompt.user``,
|
|
160
|
+
connectionId,
|
|
161
|
+
model,
|
|
162
|
+
messages,
|
|
163
|
+
reasoningEffort: session.agent.spec.reasoningEffort,
|
|
164
|
+
});
|
|
165
|
+
const enrichment = boundaryNote(turn, chunk instanceof ReACTChunk ? chunk : null, host);
|
|
166
|
+
completion.systemNote = enrichment.note;
|
|
167
|
+
const frozenNames = turn.tools.entries.map((entry) => entry.name);
|
|
168
|
+
completion.systemNote = joinSystemNotes([
|
|
169
|
+
completion.systemNote,
|
|
170
|
+
sideChannelReminderNote(
|
|
171
|
+
sealedActorRounds(turn),
|
|
172
|
+
SIDE_CHANNEL_REMINDER_EVERY,
|
|
173
|
+
frozenNames.length > 0 ? frozenNames : null,
|
|
174
|
+
),
|
|
175
|
+
]);
|
|
176
|
+
runInAction(() => {
|
|
177
|
+
const next = new CompletionJob({ expectation: completion, managesResponse: true });
|
|
178
|
+
turn.admitRound(chunk instanceof ReACTChunk ? chunk : null, next);
|
|
179
|
+
host.notes.consume(session.sessionId, enrichment.noteIds, turn.uuid);
|
|
180
|
+
});
|
|
181
|
+
// R3: read the live inputs once (wall clock, prior warmUntilMs, estimated savings) and record them.
|
|
182
|
+
stampRetentionClock(completion);
|
|
183
|
+
host.diagnostics?.step?.(`turn round ${turn.doneWork.length + turn.queue.length} ${turn.prompt}`);
|
|
184
|
+
host.diagnostics?.trace?.("harness:turn", `round ${turn.uuid} queued=${turn.queue.length}`);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case chunk instanceof CompletionJob: {
|
|
188
|
+
const pew = chunk.expectation;
|
|
189
|
+
if (pew == null) break;
|
|
190
|
+
if (!pew.isDone) await host.resolvers.completion(chunk, signal);
|
|
191
|
+
if (turnLoopStopped(turn, signal)) return;
|
|
192
|
+
if (chunk.responseExpanded) break;
|
|
193
|
+
if (pew.outcome !== "successful") {
|
|
194
|
+
turn.finishResponseExpansion(chunk);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
const filtered = pew.finishReason === CONTENT_FILTER_STOP;
|
|
198
|
+
const refusals = pew.responseCalls.filter((call) => filtered || call.dropReason !== "");
|
|
199
|
+
const calls = pew.responseCalls.filter((call) => !refusals.includes(call)).map(toolCallFromResponse);
|
|
200
|
+
const order = new ResponseCallOrder({
|
|
201
|
+
entryClass: (name) => catalogOf(session)?.catalogEntry(name)?.class,
|
|
202
|
+
remoteMcp: (key) => host.remoteMcp?.(key) ?? false,
|
|
203
|
+
});
|
|
204
|
+
let cursor = 0;
|
|
205
|
+
const record = (make: () => ToolCallJob | ProgrammaticJob) =>
|
|
206
|
+
runInAction(() => {
|
|
207
|
+
const existing = chunk.responseWork[cursor++];
|
|
208
|
+
if (existing != null) {
|
|
209
|
+
order.place(existing);
|
|
210
|
+
return existing;
|
|
211
|
+
}
|
|
212
|
+
const tool = make();
|
|
213
|
+
order.place(tool);
|
|
214
|
+
turn.recordResponseWork(chunk, tool);
|
|
215
|
+
return tool;
|
|
216
|
+
});
|
|
217
|
+
for (const call of refusals) {
|
|
218
|
+
record(
|
|
219
|
+
() =>
|
|
220
|
+
new SystemToolJob({
|
|
221
|
+
toolCallId: call.toolCallId,
|
|
222
|
+
name: call.name,
|
|
223
|
+
argsJson: "{}",
|
|
224
|
+
humanApprove: false,
|
|
225
|
+
completed: true,
|
|
226
|
+
continuationJson: JSON.stringify({ kind: "none" }),
|
|
227
|
+
triggersReactLoop: false,
|
|
228
|
+
outcomeJson: JSON.stringify({ ok: false, text: refusalText(call.dropReason, filtered) }),
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
// Early metadata must settle before other calls become reachable. The durable cursor
|
|
233
|
+
// makes re-entry safe even when a park happens in this wait.
|
|
234
|
+
const recorded = new Map(chunk.responseWork.map((job, index) => [job.toolCallId, { job, index }]));
|
|
235
|
+
const isEarly = (call: ToolCall) => {
|
|
236
|
+
const prior = recorded.get(call.id)?.job;
|
|
237
|
+
if (prior != null) return prior.isSideChannel;
|
|
238
|
+
const entry =
|
|
239
|
+
catalogOf(session)?.catalogEntry(call.function.name) ??
|
|
240
|
+
behaviorEntriesOf(session).find((row) => row.wireName === call.function.name);
|
|
241
|
+
return entry?.isSideChannel === true;
|
|
242
|
+
};
|
|
243
|
+
const early = calls
|
|
244
|
+
.filter(isEarly)
|
|
245
|
+
.sort((a, b) => (recorded.get(a.id)?.index ?? Infinity) - (recorded.get(b.id)?.index ?? Infinity));
|
|
246
|
+
const ordinary = calls.filter((candidate) => !isEarly(candidate));
|
|
247
|
+
for (const call of early) {
|
|
248
|
+
const tool = record(() => host.admit(call));
|
|
249
|
+
if (!tool.completed && (await host.resolvers.work(tool, signal)) === "aborted") return;
|
|
250
|
+
if (turnLoopStopped(turn, signal)) return;
|
|
251
|
+
}
|
|
252
|
+
for (const call of ordinary) {
|
|
253
|
+
record(() => host.admit(call));
|
|
254
|
+
}
|
|
255
|
+
const read = compactRead(host.compactKeepTurns, session.kvs);
|
|
256
|
+
const eat = Transcript.fromCells(historyBehind(turn, read));
|
|
257
|
+
const zones = sliceCompactZones(eat, keepFloors(turn.contextNeed, session.kvs, false, read.configFloor));
|
|
258
|
+
const zoneSlice = { prefix: zones.prefix, keep: zones.keep };
|
|
259
|
+
const forced = tailCompactCandidate(turn, parseCompactTrigger(host.hardCompactTrigger), zoneSlice);
|
|
260
|
+
let forcedCompact: CompactJob | null = null;
|
|
261
|
+
if (forced != null) {
|
|
262
|
+
const job = new CompactJob({ reason: forced });
|
|
263
|
+
// F8: the cut's inputs are facts of this job from here on, not live settings.
|
|
264
|
+
stampCompactInputs(job, read.configFloor, session.kvs);
|
|
265
|
+
forcedCompact = job;
|
|
266
|
+
}
|
|
267
|
+
const option = compactDecision(turn, zoneSlice);
|
|
268
|
+
const cutoff = option == null ? null : ambientCutoff(zones.keep);
|
|
269
|
+
const summarizer = ambientModel(session.kvs, model);
|
|
270
|
+
if (option != null && cutoff != null && connectionId !== "" && summarizer !== "") {
|
|
271
|
+
host.startAmbientCompact({
|
|
272
|
+
home: turn,
|
|
273
|
+
cutoff,
|
|
274
|
+
prefix: zones.prefix,
|
|
275
|
+
focus: option.focus,
|
|
276
|
+
connectionId,
|
|
277
|
+
model: summarizer,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
turn.finishResponseExpansion(chunk, forcedCompact);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case chunk instanceof ToolCallJob:
|
|
284
|
+
case chunk instanceof ProgrammaticJob: {
|
|
285
|
+
host.diagnostics?.trace?.(
|
|
286
|
+
"harness:turn",
|
|
287
|
+
`work ${turn.uuid} ${chunk instanceof ToolCallJob ? chunk.name : "scheme"} ${chunk.toolCallId}`,
|
|
288
|
+
);
|
|
289
|
+
if (!chunk.completed && (await host.resolvers.work(chunk, signal)) === "aborted") return;
|
|
290
|
+
if (turnLoopStopped(turn, signal)) return;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
default:
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function blockAdmission(turn: SessionTurn, chunk: TurnPromptChunk | ReACTChunk, gate: RoundGate): void {
|
|
300
|
+
runInAction(() => {
|
|
301
|
+
if (chunk instanceof ReACTChunk && chunk.decision != null) {
|
|
302
|
+
turn.commitBoundary(chunk, { ...chunk.decision, kind: "blocked", ...gate }, chunk.contextNotes, "");
|
|
303
|
+
} else turn.stopLoop(gate.detail);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function compactRead(
|
|
308
|
+
configFloor: number,
|
|
309
|
+
kv: Readonly<Record<string, string>>,
|
|
310
|
+
): { readonly configFloor: number; readonly kv: Readonly<Record<string, string>> } {
|
|
311
|
+
return { configFloor: Math.max(configFloor, 1), kv };
|
|
312
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { CompactJob, CompletionJob, ProgrammaticJob, ToolCallJob } from "@inhuman.tools/harness-model/session";
|
|
2
|
+
import type { CompleteRequest, CompleteResult } from "@inhuman.tools/harness-provider-sdk";
|
|
3
|
+
|
|
4
|
+
import { runCompactJob, type CompactHost } from "../session/compact-actor.js";
|
|
5
|
+
|
|
6
|
+
/** Resolution changes the work node. The iterator reads that node afterward. */
|
|
7
|
+
export interface TurnResolvers {
|
|
8
|
+
completion(job: CompletionJob, signal: AbortSignal): Promise<void>;
|
|
9
|
+
work(job: ToolCallJob | ProgrammaticJob, signal: AbortSignal): Promise<"settled" | "aborted">;
|
|
10
|
+
compact(job: CompactJob, signal: AbortSignal): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TurnResolverHost {
|
|
14
|
+
complete(request: CompleteRequest): Promise<CompleteResult>;
|
|
15
|
+
join(job: ToolCallJob | ProgrammaticJob, signal: AbortSignal): Promise<"settled" | "aborted">;
|
|
16
|
+
compactHost(): CompactHost;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Production effects; tests substitute this whole strategy without a daemon. */
|
|
20
|
+
export function createTurnResolvers(host: TurnResolverHost): TurnResolvers {
|
|
21
|
+
return {
|
|
22
|
+
async completion(job, signal) {
|
|
23
|
+
const pew = job.expectation;
|
|
24
|
+
if (pew == null || pew.isDone) return;
|
|
25
|
+
try {
|
|
26
|
+
await host.complete({
|
|
27
|
+
connectionId: pew.connectionId,
|
|
28
|
+
model: pew.model,
|
|
29
|
+
promptText: pew.userPrompt,
|
|
30
|
+
round: pew,
|
|
31
|
+
signal,
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// A park aborts with the reason "park" and leaves the round open for the next wrap.
|
|
35
|
+
if (!signal.aborted && pew.outcome == null && error !== "park") {
|
|
36
|
+
pew.fail("crash", error instanceof Error ? error.message : String(error), "null");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
work: (job, signal) => host.join(job, signal),
|
|
41
|
+
compact: (job, signal) => runCompactJob(job, host.compactHost(), signal),
|
|
42
|
+
};
|
|
43
|
+
}
|
package/src/session/cancel.ts
CHANGED
|
@@ -2,12 +2,20 @@
|
|
|
2
2
|
* Stop predicate and leftover rejection for a turn. Accepted-work Calls stay.
|
|
3
3
|
* In-flight BODY abort is the caller's `abort`.
|
|
4
4
|
*/
|
|
5
|
-
import { ApprovableJob, CallJob, SessionTurn } from "@inhuman.tools/harness-model";
|
|
6
|
-
import { walkWork } from "@inhuman.tools/plexus-expectations";
|
|
5
|
+
import { ApprovableJob, CallJob, SessionTurn, type SessionRoot } from "@inhuman.tools/harness-model/session";
|
|
6
|
+
import { walkWork, type WorkContainer } from "@inhuman.tools/plexus-expectations";
|
|
7
7
|
|
|
8
8
|
import { shouldSkipTurnCancel } from "../agent-calls/acceptance.js";
|
|
9
9
|
import { type CallsCatalog } from "../agent-calls/index.js";
|
|
10
|
-
import { cancelUnstartedTool } from "../tool-runtime/
|
|
10
|
+
import { cancelUnstartedTool } from "../tool-runtime/approval/approved.js";
|
|
11
|
+
|
|
12
|
+
/** Canonical work ownership includes nested and retained background turns, not just head ancestry. */
|
|
13
|
+
export function isWorkReachable(session: SessionRoot, work: WorkContainer): boolean {
|
|
14
|
+
for (const node of walkWork(session)) {
|
|
15
|
+
if (node === work) return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
11
19
|
|
|
12
20
|
export function turnLoopStopped(turn: SessionTurn, signal: AbortSignal): boolean {
|
|
13
21
|
return signal.aborted || turn.reducerExited || turn.cancelRequested;
|