Repository navigation
Expand file tree
/
Copy paththreads.ts
More file actions
134 lines (123 loc) · 5.45 KB
/
Copy paththreads.ts
File metadata and controls
134 lines (123 loc) · 5.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import { useAtomValue } from "@effect/atom-react";
import { enabledEnvironmentIds } from "@t3tools/client-runtime/state/connections";
import { arrayElementsEqual } from "@t3tools/client-runtime/state/entities";
import {
createEnvironmentThreadDetailAtoms,
createEnvironmentThreadShellAtoms,
createEnvironmentThreadStateAtoms,
EMPTY_ENVIRONMENT_THREAD_STATE,
type EnvironmentThreadState,
createThreadEnvironmentAtoms,
createThreadFamilyAtoms,
} from "@t3tools/client-runtime/state/threads";
import type { EnvironmentId, OrchestrationV2ThreadShell, ThreadId } from "@t3tools/contracts";
import * as Option from "effect/Option";
import { AsyncResult, Atom } from "effect/reactivity";
import { useEffect } from "react";
import { environmentCatalog } from "../connection/catalog";
import { connectionAtomRuntime } from "../connection/runtime";
import { appAtomRegistry } from "../rpc/atomRegistry";
import { environmentShell } from "./shell";
const threadFamilies = createThreadFamilyAtoms(connectionAtomRuntime, environmentShell.stateAtom);
export const threadEnvironment = createThreadEnvironmentAtoms(
connectionAtomRuntime,
threadFamilies.snapshotAtom,
);
const environmentThreads = createEnvironmentThreadStateAtoms(connectionAtomRuntime);
export const environmentThreadDetails = createEnvironmentThreadDetailAtoms(
environmentThreads.stateAtom,
);
export const environmentThreadShells = createEnvironmentThreadShellAtoms({
catalogValueAtom: environmentCatalog.catalogValueAtom,
snapshotAtom: threadEnvironment.snapshotAtom,
});
/** Keeps the subagent tree under `rootThreadId` in the thread list while the caller shows it. */
export function useThreadFamily(
environmentId: EnvironmentId | null,
rootThreadId: ThreadId | null,
): void {
useEffect(() => {
if (environmentId === null || rootThreadId === null) return;
return threadFamilies.mount(appAtomRegistry, environmentId, rootThreadId);
}, [environmentId, rootThreadId]);
}
const EMPTY_THREAD_STATE_ATOM = Atom.make(AsyncResult.success(EMPTY_ENVIRONMENT_THREAD_STATE)).pipe(
Atom.withLabel("web-environment-thread:empty"),
);
export function useEnvironmentThread(
environmentId: EnvironmentId | null,
threadId: ThreadId | null,
): EnvironmentThreadState {
const result = useAtomValue(
environmentId !== null && threadId !== null
? environmentThreads.stateAtom(environmentId, threadId)
: EMPTY_THREAD_STATE_ATOM,
);
const state = Option.getOrElse(
AsyncResult.value(result),
() => EMPTY_ENVIRONMENT_THREAD_STATE,
) as EnvironmentThreadState;
return state;
}
const isRunning = (status: string) =>
status === "preparing" || status === "queued" || status === "starting" || status === "running";
type KeptThreads = ReadonlyMap<EnvironmentId, ReadonlySet<ThreadId>>;
function isDetailDone<E>(result: AsyncResult.AsyncResult<EnvironmentThreadState, E>): boolean {
if (!AsyncResult.isSuccess(result)) return true;
const { status, data, error } = result.value;
if (status === "deleted" || Option.isSome(error)) return true;
return (
status === "live" &&
!Option.exists(data, (thread) => thread.runs.some((run) => isRunning(run.status)))
);
}
/** Keeps stopped threads mounted until their independent detail stream is live and also reports stopped. */
export function createRunningThreadKeepAliveAtom<E>(input: {
readonly environmentIdsAtom: Atom.Atom<ReadonlyArray<EnvironmentId>>;
readonly threadsAtom: (
environmentId: EnvironmentId,
) => Atom.Atom<ReadonlyArray<Pick<OrchestrationV2ThreadShell, "id" | "status">>>;
readonly stateAtom: (
environmentId: EnvironmentId,
threadId: ThreadId,
) => Atom.Atom<AsyncResult.AsyncResult<EnvironmentThreadState, E>>;
}) {
const runningThreadIdsAtom = Atom.family((environmentId: EnvironmentId) => {
let previous: ReadonlyArray<ThreadId> = [];
return Atom.make((get) => {
const running = get(input.threadsAtom(environmentId)).flatMap((thread) =>
isRunning(thread.status) ? [thread.id] : [],
);
if (arrayElementsEqual(previous, running)) return previous;
previous = running;
return running;
}).pipe(Atom.withLabel(`web-running-thread-ids:${environmentId}`));
});
return Atom.make((get): KeptThreads => {
const previous = Option.getOrUndefined(get.self<KeptThreads>());
const kept = new Map<EnvironmentId, ReadonlySet<ThreadId>>();
for (const environmentId of get(input.environmentIdsAtom)) {
const threadIds = new Set(get(runningThreadIdsAtom(environmentId)));
for (const threadId of previous?.get(environmentId) ?? []) {
if (threadIds.has(threadId)) continue;
const stateAtom = input.stateAtom(environmentId, threadId);
// A get dependency would hold stopped streams open until another change rebuilds this atom.
if (isDetailDone(get.once(stateAtom))) continue;
threadIds.add(threadId);
get.subscribe(stateAtom, (state) => {
if (isDetailDone(state)) get.refreshSelf();
});
}
for (const threadId of threadIds) get.mount(input.stateAtom(environmentId, threadId));
kept.set(environmentId, threadIds);
}
return kept;
}).pipe(Atom.withLabel("web-running-thread-keep-alive"));
}
export const runningThreadKeepAliveAtom = createRunningThreadKeepAliveAtom({
environmentIdsAtom: Atom.map(environmentCatalog.catalogValueAtom, (catalog) => [
...enabledEnvironmentIds(catalog),
]),
threadsAtom: environmentThreadShells.environmentThreadsAtom,
stateAtom: environmentThreads.stateAtom,
});