t3code-cli 0.10.0 → 0.11.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 +33 -1
- package/dist/application.js +2 -2
- package/dist/auth.js +1 -1
- package/dist/bin.js +1566 -351
- package/dist/cli.js +1 -1
- package/dist/config.js +1 -1
- package/dist/connection.js +1 -11
- package/dist/index.js +1 -1
- package/dist/layout.js +1 -1
- package/dist/node.js +2 -0
- package/dist/orchestration.js +2 -2
- package/dist/rpc.js +2 -2
- package/dist/runtime.js +1 -1
- package/dist/scope.js +1 -1
- package/dist/shared.js +1740 -767
- package/dist/src/application/index.d.ts +3 -2
- package/dist/src/application/layer.d.ts +82 -841
- package/dist/src/application/model-selection.d.ts +15 -0
- package/dist/src/application/models.d.ts +1 -87
- package/dist/src/application/project-commands.d.ts +9 -0
- package/dist/src/application/projects.d.ts +13 -135
- package/dist/src/application/service.d.ts +84 -5
- package/dist/src/application/shell-sequence.d.ts +2 -12
- package/dist/src/application/terminals.d.ts +66 -0
- package/dist/src/application/thread-commands.d.ts +47 -0
- package/dist/src/application/thread-update.d.ts +8 -0
- package/dist/src/application/thread-wait.d.ts +3 -14
- package/dist/src/application/threads.d.ts +21 -50
- package/dist/src/cli/flags.d.ts +3 -0
- package/dist/src/cli/output-format.d.ts +2 -0
- package/dist/src/domain/error.d.ts +14 -1
- package/dist/src/domain/helpers.d.ts +0 -9
- package/dist/src/domain/model-config.d.ts +29 -3
- package/dist/src/domain/thread-lifecycle.d.ts +2 -10
- package/dist/src/node/index.d.ts +2 -0
- package/dist/src/orchestration/index.d.ts +1 -0
- package/dist/src/orchestration/layer.d.ts +177 -1032
- package/dist/src/orchestration/service.d.ts +1 -0
- package/dist/src/rpc/error.d.ts +4 -2
- package/dist/src/rpc/index.d.ts +5 -0
- package/dist/src/rpc/layer.d.ts +1097 -499
- package/dist/src/rpc/operation.d.ts +5752 -0
- package/dist/src/rpc/ws-group.d.ts +327 -147
- package/dist/src/runtime/layer.d.ts +1 -1
- package/dist/t3tools.js +2 -2
- package/dist/upstream-t3code/packages/contracts/src/environmentHttp.d.ts +10 -33
- package/dist/upstream-t3code/packages/contracts/src/ipc.d.ts +0 -6
- package/dist/upstream-t3code/packages/contracts/src/orchestration.d.ts +1 -657
- package/dist/upstream-t3code/packages/contracts/src/provider.d.ts +0 -14
- package/dist/upstream-t3code/packages/contracts/src/providerRuntime.d.ts +1 -175
- package/dist/upstream-t3code/packages/contracts/src/rpc.d.ts +0 -492
- package/dist/upstream-t3code/packages/contracts/src/server.d.ts +0 -45
- package/dist/upstream-t3code/packages/contracts/src/settings.d.ts +0 -21
- package/dist/upstream-t3code/packages/contracts/src/terminal.d.ts +3 -6
- package/package.json +6 -2
- package/src/application/index.ts +28 -2
- package/src/application/layer.ts +34 -5
- package/src/application/model-selection.ts +68 -2
- package/src/application/models.ts +14 -14
- package/src/application/project-commands.ts +15 -0
- package/src/application/projects.ts +43 -13
- package/src/application/service.ts +196 -77
- package/src/application/shell-sequence.ts +5 -7
- package/src/application/terminals.ts +207 -0
- package/src/application/thread-commands.test.ts +43 -0
- package/src/application/thread-commands.ts +81 -0
- package/src/application/thread-update.ts +72 -0
- package/src/application/thread-wait.ts +46 -47
- package/src/application/threads.test.ts +166 -0
- package/src/application/threads.ts +123 -40
- package/src/bin.ts +2 -0
- package/src/cli/app.ts +2 -0
- package/src/cli/confirm.ts +31 -0
- package/src/cli/error.ts +26 -3
- package/src/cli/flags.ts +15 -0
- package/src/cli/input/layer.ts +20 -0
- package/src/cli/input/service.ts +1 -0
- package/src/cli/output-format.ts +6 -2
- package/src/cli/project-format.ts +7 -0
- package/src/cli/project.ts +2 -1
- package/src/cli/projects/delete.ts +50 -0
- package/src/cli/self-action.ts +34 -0
- package/src/cli/terminal/attach.ts +32 -0
- package/src/cli/terminal/commands.test.ts +101 -0
- package/src/cli/terminal/create.ts +54 -0
- package/src/cli/terminal/destroy.ts +60 -0
- package/src/cli/terminal/encoding.test.ts +63 -0
- package/src/cli/terminal/encoding.ts +23 -0
- package/src/cli/terminal/error.ts +14 -0
- package/src/cli/terminal/io-node-layer.ts +82 -0
- package/src/cli/terminal/io-service.ts +25 -0
- package/src/cli/terminal/list.ts +35 -0
- package/src/cli/terminal/read.ts +102 -0
- package/src/cli/terminal/scope.ts +30 -0
- package/src/cli/terminal/shared.ts +250 -0
- package/src/cli/terminal/stream.ts +64 -0
- package/src/cli/terminal/wait.test.ts +146 -0
- package/src/cli/terminal/wait.ts +222 -0
- package/src/cli/terminal/write.ts +132 -0
- package/src/cli/terminal-format.ts +167 -0
- package/src/cli/terminal.ts +26 -0
- package/src/cli/thread-format.test.ts +32 -0
- package/src/cli/thread-format.ts +8 -1
- package/src/cli/thread.ts +8 -0
- package/src/cli/threads/archive.ts +11 -18
- package/src/cli/threads/delete.ts +61 -0
- package/src/cli/threads/interrupt.ts +52 -0
- package/src/cli/threads/list.test.ts +69 -0
- package/src/cli/threads/list.ts +15 -3
- package/src/cli/threads/send.ts +22 -2
- package/src/cli/threads/unarchive.ts +44 -0
- package/src/cli/threads/update.ts +150 -0
- package/src/domain/error.ts +20 -1
- package/src/domain/model-config.ts +4 -0
- package/src/domain/thread-lifecycle.ts +14 -0
- package/src/node/index.ts +2 -0
- package/src/orchestration/index.ts +1 -0
- package/src/orchestration/layer.ts +33 -85
- package/src/orchestration/service.ts +4 -0
- package/src/rpc/error.ts +10 -0
- package/src/rpc/index.ts +5 -0
- package/src/rpc/operation.ts +83 -0
- package/src/rpc/ws-group.ts +24 -0
- package/src/runtime/layer.ts +7 -2
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as Crypto from "effect/Crypto";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import type { ModelSelection } from "#t3tools/contracts";
|
|
4
|
+
|
|
5
|
+
import { ModelSelectionError, ThreadEventError } from "../domain/error.ts";
|
|
6
|
+
import { T3Orchestration } from "../orchestration/service.ts";
|
|
7
|
+
import { mergeModelOptions, resolveUpdateModelSelection } from "./model-selection.ts";
|
|
8
|
+
import type { UpdateThreadInput } from "./service.ts";
|
|
9
|
+
import { makeThreadMetaUpdateCommand } from "./thread-commands.ts";
|
|
10
|
+
|
|
11
|
+
export function makeUpdateThread() {
|
|
12
|
+
return Effect.fn("T3ApplicationLive.updateThread")(function* (input: UpdateThreadInput) {
|
|
13
|
+
const orchestration = yield* T3Orchestration;
|
|
14
|
+
const crypto = yield* Crypto.Crypto;
|
|
15
|
+
const hasProvider = input.provider !== undefined && input.provider.length > 0;
|
|
16
|
+
const hasModel = input.model !== undefined && input.model.length > 0;
|
|
17
|
+
const options = input.options;
|
|
18
|
+
const hasOptions = options !== undefined && options.length > 0;
|
|
19
|
+
const thread = yield* orchestration.getThreadSnapshot(input.threadId);
|
|
20
|
+
let modelSelection: ModelSelection | undefined;
|
|
21
|
+
if (hasProvider || hasModel) {
|
|
22
|
+
const snapshot = yield* orchestration.getShellSnapshot();
|
|
23
|
+
const project = snapshot.projects.find((entry) => entry.id === thread.projectId);
|
|
24
|
+
if (project === undefined) {
|
|
25
|
+
return yield* Effect.fail(
|
|
26
|
+
new ThreadEventError({
|
|
27
|
+
message: `project not found for thread: ${input.threadId}`,
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const serverConfig = yield* orchestration.getServerConfig();
|
|
32
|
+
if (hasProvider && thread.session !== null && thread.session.status !== "stopped") {
|
|
33
|
+
const sessionProviderId =
|
|
34
|
+
thread.session.providerInstanceId ?? thread.modelSelection.instanceId;
|
|
35
|
+
const sessionProvider = serverConfig.providers.find(
|
|
36
|
+
(provider) => provider.instanceId === sessionProviderId,
|
|
37
|
+
);
|
|
38
|
+
const targetProvider = serverConfig.providers.find(
|
|
39
|
+
(provider) => provider.instanceId === input.provider,
|
|
40
|
+
);
|
|
41
|
+
if (
|
|
42
|
+
sessionProvider !== undefined &&
|
|
43
|
+
targetProvider !== undefined &&
|
|
44
|
+
targetProvider.driver !== sessionProvider.driver
|
|
45
|
+
) {
|
|
46
|
+
return yield* Effect.fail(
|
|
47
|
+
new ModelSelectionError({
|
|
48
|
+
message: `thread ${input.threadId} is bound to ${sessionProvider.driver} provider ${sessionProviderId}; cannot update provider to ${targetProvider.driver} provider ${input.provider}`,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
modelSelection = yield* resolveUpdateModelSelection({
|
|
54
|
+
current: thread.modelSelection,
|
|
55
|
+
...(hasProvider ? { provider: input.provider } : {}),
|
|
56
|
+
...(hasModel ? { model: input.model } : {}),
|
|
57
|
+
...(hasOptions ? { options } : {}),
|
|
58
|
+
project,
|
|
59
|
+
serverConfig,
|
|
60
|
+
});
|
|
61
|
+
} else if (hasOptions) {
|
|
62
|
+
modelSelection = mergeModelOptions(thread.modelSelection, options);
|
|
63
|
+
}
|
|
64
|
+
const command = yield* makeThreadMetaUpdateCommand(input.threadId, {
|
|
65
|
+
...(input.title !== undefined ? { title: input.title } : {}),
|
|
66
|
+
...(modelSelection !== undefined ? { modelSelection } : {}),
|
|
67
|
+
...(input.branch !== undefined ? { branch: input.branch } : {}),
|
|
68
|
+
...(input.worktreePath !== undefined ? { worktreePath: input.worktreePath } : {}),
|
|
69
|
+
}).pipe(Effect.provideService(Crypto.Crypto, crypto));
|
|
70
|
+
return yield* orchestration.dispatch(command);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -4,7 +4,7 @@ import * as Stream from "effect/Stream";
|
|
|
4
4
|
import type { OrchestrationMessage, OrchestrationThread } from "#t3tools/contracts";
|
|
5
5
|
|
|
6
6
|
import { ThreadSessionError } from "../domain/error.ts";
|
|
7
|
-
import
|
|
7
|
+
import { T3Orchestration } from "../orchestration/service.ts";
|
|
8
8
|
import type { WaitEvent } from "./service.ts";
|
|
9
9
|
import {
|
|
10
10
|
applyThreadEvent,
|
|
@@ -15,60 +15,59 @@ import {
|
|
|
15
15
|
threadStatus,
|
|
16
16
|
} from "../domain/thread-lifecycle.ts";
|
|
17
17
|
|
|
18
|
-
export function watchThread(input: {
|
|
19
|
-
readonly orchestration: Orchestration;
|
|
20
|
-
readonly threadId: string;
|
|
21
|
-
}) {
|
|
18
|
+
export function watchThread(input: { readonly threadId: string }) {
|
|
22
19
|
let current: OrchestrationThread | undefined;
|
|
23
20
|
let currentMessages: Map<string, OrchestrationMessage> | undefined;
|
|
24
|
-
return Stream.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
return Stream.unwrap(
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
const orchestration = yield* T3Orchestration;
|
|
24
|
+
return Stream.scoped(
|
|
25
|
+
orchestration.watchThreadItems(input.threadId).pipe(
|
|
26
|
+
Stream.flatMap((item) => {
|
|
27
|
+
if (item.kind === "snapshot") {
|
|
28
|
+
const messages = new Map<string, OrchestrationMessage>();
|
|
29
|
+
for (const message of item.snapshot.thread.messages) {
|
|
30
|
+
messages.set(messageKey(message), message);
|
|
31
|
+
}
|
|
32
|
+
current = item.snapshot.thread;
|
|
33
|
+
currentMessages = messages;
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
const events: Array<WaitEvent> = [
|
|
36
|
+
{ type: "thread", thread: current },
|
|
37
|
+
{ type: "status", status: threadStatus(current), threadId: current.id },
|
|
38
|
+
];
|
|
39
|
+
if (!isThreadActive(current) && isThreadCompleteEnough(current)) {
|
|
40
|
+
events.push({ type: "done", thread: current });
|
|
41
|
+
}
|
|
42
|
+
return Stream.fromIterable(events);
|
|
43
|
+
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
if (current === undefined || currentMessages === undefined) {
|
|
46
|
+
return Stream.fail(
|
|
47
|
+
new ThreadSessionError({
|
|
48
|
+
message: `thread stream event received before snapshot: ${input.threadId}`,
|
|
49
|
+
threadId: input.threadId,
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
current = applyThreadEvent(current, item.event, currentMessages);
|
|
55
|
+
const message = messageFromEvent(item.event, currentMessages);
|
|
56
|
+
const events: Array<WaitEvent> = message !== null ? [{ type: "message", message }] : [];
|
|
57
|
+
events.push({ type: "status", status: threadStatus(current), threadId: current.id });
|
|
58
|
+
if (!isThreadActive(current) && isThreadCompleteEnough(current)) {
|
|
59
|
+
events.push({ type: "done", thread: current });
|
|
60
|
+
}
|
|
61
|
+
return Stream.fromIterable(events);
|
|
62
|
+
}),
|
|
63
|
+
Stream.takeUntil((event) => event.type === "done"),
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
}),
|
|
65
67
|
);
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
export function waitForThread(input: {
|
|
69
|
-
readonly orchestration: Orchestration;
|
|
70
|
-
readonly threadId: string;
|
|
71
|
-
}) {
|
|
70
|
+
export function waitForThread(input: { readonly threadId: string }) {
|
|
72
71
|
return watchThread(input).pipe(
|
|
73
72
|
Stream.runLast,
|
|
74
73
|
Effect.flatMap((event) => {
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import "vite-plus/test/config";
|
|
2
|
+
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
6
|
+
import { assert, describe, it } from "@effect/vitest";
|
|
7
|
+
import { fromPartial } from "@total-typescript/shoehorn";
|
|
8
|
+
import type {
|
|
9
|
+
ClientOrchestrationCommand,
|
|
10
|
+
OrchestrationShellSnapshot,
|
|
11
|
+
OrchestrationThread,
|
|
12
|
+
OrchestrationThreadShell,
|
|
13
|
+
} from "#t3tools/contracts";
|
|
14
|
+
|
|
15
|
+
import { NodeEnvironmentLive } from "../environment/layer.ts";
|
|
16
|
+
import { T3Orchestration, type Orchestration } from "../orchestration/service.ts";
|
|
17
|
+
import { makeThreadApplication } from "./threads.ts";
|
|
18
|
+
|
|
19
|
+
function makeThread(
|
|
20
|
+
id: string,
|
|
21
|
+
projectId: string,
|
|
22
|
+
archivedAt: string | null,
|
|
23
|
+
): OrchestrationThreadShell {
|
|
24
|
+
return fromPartial({
|
|
25
|
+
id,
|
|
26
|
+
projectId,
|
|
27
|
+
archivedAt,
|
|
28
|
+
title: `thread-${id}`,
|
|
29
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeSnapshot(threads: OrchestrationThreadShell[]): OrchestrationShellSnapshot {
|
|
34
|
+
return fromPartial({
|
|
35
|
+
projects: [{ id: "proj-1", workspaceRoot: "/workspace" }],
|
|
36
|
+
threads,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeOrchestrationLayer(input: {
|
|
41
|
+
readonly active?: OrchestrationShellSnapshot;
|
|
42
|
+
readonly archived?: OrchestrationShellSnapshot;
|
|
43
|
+
readonly threadSnapshot?: OrchestrationThread;
|
|
44
|
+
readonly onDispatch?: (command: ClientOrchestrationCommand) => void;
|
|
45
|
+
}) {
|
|
46
|
+
return Layer.succeed(
|
|
47
|
+
T3Orchestration,
|
|
48
|
+
fromPartial<Orchestration>({
|
|
49
|
+
getShellSnapshot: () => Effect.succeed(input.active ?? makeSnapshot([])),
|
|
50
|
+
getArchivedShellSnapshot: () => Effect.succeed(input.archived ?? makeSnapshot([])),
|
|
51
|
+
getThreadSnapshot: () => Effect.succeed(input.threadSnapshot ?? fromPartial({})),
|
|
52
|
+
dispatch: (command: ClientOrchestrationCommand) => {
|
|
53
|
+
input.onDispatch?.(command);
|
|
54
|
+
return Effect.succeed({ sequence: 42 });
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const testLayer = (orchestration: Layer.Layer<T3Orchestration>) =>
|
|
61
|
+
Layer.mergeAll(orchestration, NodeServices.layer, NodeEnvironmentLive);
|
|
62
|
+
|
|
63
|
+
describe("interruptThread", () => {
|
|
64
|
+
it.layer(NodeServices.layer)("interruptThread", (t) => {
|
|
65
|
+
t.effect("dispatches thread.turn.interrupt with activeTurnId from snapshot", () =>
|
|
66
|
+
Effect.gen(function* () {
|
|
67
|
+
let dispatched: ClientOrchestrationCommand | undefined;
|
|
68
|
+
const threadSnapshot = fromPartial<OrchestrationThread>({
|
|
69
|
+
id: "thread-1",
|
|
70
|
+
session: {
|
|
71
|
+
activeTurnId: "turn-1",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const app = yield* makeThreadApplication().pipe(
|
|
75
|
+
Effect.provide(
|
|
76
|
+
testLayer(
|
|
77
|
+
makeOrchestrationLayer({
|
|
78
|
+
threadSnapshot,
|
|
79
|
+
onDispatch: (command) => {
|
|
80
|
+
dispatched = command;
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const dispatch = yield* app.interruptThread("thread-1");
|
|
88
|
+
assert.equal(dispatch.sequence, 42);
|
|
89
|
+
assert.equal(dispatched?.type, "thread.turn.interrupt");
|
|
90
|
+
if (dispatched?.type === "thread.turn.interrupt") {
|
|
91
|
+
assert.equal(dispatched.threadId, "thread-1");
|
|
92
|
+
assert.equal(dispatched.turnId, "turn-1");
|
|
93
|
+
}
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("listThreads", () => {
|
|
100
|
+
it.layer(NodeServices.layer)("listThreads", (t) => {
|
|
101
|
+
t.effect("returns active threads by default", () =>
|
|
102
|
+
Effect.gen(function* () {
|
|
103
|
+
const active = makeSnapshot([
|
|
104
|
+
makeThread("active-1", "proj-1", null),
|
|
105
|
+
makeThread("active-2", "proj-1", null),
|
|
106
|
+
]);
|
|
107
|
+
const archived = makeSnapshot([
|
|
108
|
+
makeThread("archived-1", "proj-1", "2026-01-01T00:00:00.000Z"),
|
|
109
|
+
]);
|
|
110
|
+
const app = yield* makeThreadApplication().pipe(
|
|
111
|
+
Effect.provide(testLayer(makeOrchestrationLayer({ active, archived }))),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const result = yield* app.listThreads("proj-1");
|
|
115
|
+
assert.deepEqual(
|
|
116
|
+
result.threads.map((thread) => thread.id),
|
|
117
|
+
["active-1", "active-2"],
|
|
118
|
+
);
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
t.effect("returns archived threads when include is archived", () =>
|
|
123
|
+
Effect.gen(function* () {
|
|
124
|
+
const active = makeSnapshot([makeThread("active-1", "proj-1", null)]);
|
|
125
|
+
const archived = makeSnapshot([
|
|
126
|
+
makeThread("archived-1", "proj-1", "2026-01-01T00:00:00.000Z"),
|
|
127
|
+
makeThread("archived-2", "proj-1", "2026-01-02T00:00:00.000Z"),
|
|
128
|
+
]);
|
|
129
|
+
const app = yield* makeThreadApplication().pipe(
|
|
130
|
+
Effect.provide(testLayer(makeOrchestrationLayer({ active, archived }))),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const result = yield* app.listThreads("proj-1", { include: "archived" });
|
|
134
|
+
assert.deepEqual(
|
|
135
|
+
result.threads.map((thread) => thread.id),
|
|
136
|
+
["archived-1", "archived-2"],
|
|
137
|
+
);
|
|
138
|
+
assert.isTrue(result.threads.every((thread) => thread.archivedAt !== null));
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
t.effect("returns merged threads without duplicates when include is all", () =>
|
|
143
|
+
Effect.gen(function* () {
|
|
144
|
+
const active = makeSnapshot([
|
|
145
|
+
makeThread("active-1", "proj-1", null),
|
|
146
|
+
makeThread("shared", "proj-1", null),
|
|
147
|
+
]);
|
|
148
|
+
const archived = makeSnapshot([
|
|
149
|
+
makeThread("archived-1", "proj-1", "2026-01-01T00:00:00.000Z"),
|
|
150
|
+
makeThread("shared", "proj-1", "2026-01-02T00:00:00.000Z"),
|
|
151
|
+
]);
|
|
152
|
+
const app = yield* makeThreadApplication().pipe(
|
|
153
|
+
Effect.provide(testLayer(makeOrchestrationLayer({ active, archived }))),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const result = yield* app.listThreads("proj-1", { include: "all" });
|
|
157
|
+
assert.deepEqual(
|
|
158
|
+
result.threads.map((thread) => thread.id).toSorted(),
|
|
159
|
+
["active-1", "archived-1", "shared"].toSorted(),
|
|
160
|
+
);
|
|
161
|
+
const shared = result.threads.find((thread) => thread.id === "shared");
|
|
162
|
+
assert.equal(shared?.archivedAt, "2026-01-02T00:00:00.000Z");
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
import * as Crypto from "effect/Crypto";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
3
|
import * as Path from "effect/Path";
|
|
4
|
-
|
|
4
|
+
import * as Stream from "effect/Stream";
|
|
5
5
|
import { Environment } from "../environment/service.ts";
|
|
6
6
|
import { T3Orchestration } from "../orchestration/service.ts";
|
|
7
|
-
import { ProjectLookupError, ThreadSessionError } from "../domain/error.ts";
|
|
7
|
+
import { ProjectLookupError, ThreadLookupError, ThreadSessionError } from "../domain/error.ts";
|
|
8
8
|
import { resolveProjectScope } from "../domain/helpers.ts";
|
|
9
|
-
import { type StartThreadInput } from "./service.ts";
|
|
10
|
-
import type {
|
|
9
|
+
import { type ListThreadsInclude, type StartThreadInput } from "./service.ts";
|
|
10
|
+
import type { CallbackThreadInput, SendThreadInput } from "./service.ts";
|
|
11
|
+
import type { T3ThreadApplicationService } from "./service.ts";
|
|
12
|
+
import type { OrchestrationThreadShell } from "#t3tools/contracts";
|
|
11
13
|
import { mergeModelOptions } from "./model-selection.ts";
|
|
12
14
|
import { derivePendingApprovals, derivePendingUserInputs } from "../domain/thread-activities.ts";
|
|
13
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
sessionNeedsStopBeforeDelete,
|
|
17
|
+
threadStatus,
|
|
18
|
+
type ThreadLifecycleStatus,
|
|
19
|
+
} from "../domain/thread-lifecycle.ts";
|
|
14
20
|
import type { OrchestrationThread } from "#t3tools/contracts";
|
|
15
21
|
import type { ProviderApprovalDecision, ProviderUserInputAnswers } from "#t3tools/contracts";
|
|
16
22
|
import {
|
|
17
23
|
makeThreadApprovalRespondCommand,
|
|
18
24
|
makeThreadArchiveCommand,
|
|
25
|
+
makeThreadDeleteCommand,
|
|
26
|
+
makeThreadInterruptCommand,
|
|
27
|
+
makeThreadSessionStopCommand,
|
|
19
28
|
makeThreadStartCommands,
|
|
20
29
|
makeThreadTurnContinueCommand,
|
|
30
|
+
makeThreadUnarchiveCommand,
|
|
21
31
|
makeThreadUserInputRespondCommand,
|
|
22
32
|
} from "./thread-commands.ts";
|
|
33
|
+
import { makeUpdateThread } from "./thread-update.ts";
|
|
23
34
|
import {
|
|
24
35
|
waitForThread as waitForThreadUntilComplete,
|
|
25
36
|
watchThread as watchThreadEvents,
|
|
@@ -31,8 +42,24 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
|
|
|
31
42
|
const crypto = yield* Crypto.Crypto;
|
|
32
43
|
const path = yield* Path.Path;
|
|
33
44
|
const environment = yield* Environment;
|
|
34
|
-
const
|
|
35
|
-
|
|
45
|
+
const awaitShellSequence = (sequence: number) =>
|
|
46
|
+
waitForShellSequence({ sequence }).pipe(Effect.provideService(T3Orchestration, orchestration));
|
|
47
|
+
const awaitThreadCompletion = (threadId: string) =>
|
|
48
|
+
waitForThreadUntilComplete({ threadId }).pipe(
|
|
49
|
+
Effect.provideService(T3Orchestration, orchestration),
|
|
50
|
+
);
|
|
51
|
+
const streamThreadEvents = (threadId: string) =>
|
|
52
|
+
watchThreadEvents({ threadId }).pipe(Stream.provideService(T3Orchestration, orchestration));
|
|
53
|
+
const listThreads = Effect.fn("T3ApplicationLive.listThreads")(function* (
|
|
54
|
+
projectRef: string,
|
|
55
|
+
options?: {
|
|
56
|
+
readonly include?: ListThreadsInclude;
|
|
57
|
+
},
|
|
58
|
+
) {
|
|
59
|
+
const include = options?.include ?? "active";
|
|
60
|
+
const snapshot = yield* loadThreadsSnapshot(include).pipe(
|
|
61
|
+
Effect.provideService(T3Orchestration, orchestration),
|
|
62
|
+
);
|
|
36
63
|
const scope = yield* resolveProjectScope(snapshot, {
|
|
37
64
|
ref: projectRef,
|
|
38
65
|
}).pipe(Effect.provideService(Path.Path, path));
|
|
@@ -61,6 +88,55 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
|
|
|
61
88
|
);
|
|
62
89
|
return yield* orchestration.dispatch(command);
|
|
63
90
|
});
|
|
91
|
+
const unarchiveThread = Effect.fn("T3ApplicationLive.unarchiveThread")(function* (
|
|
92
|
+
threadId: string,
|
|
93
|
+
) {
|
|
94
|
+
const command = yield* makeThreadUnarchiveCommand(threadId).pipe(
|
|
95
|
+
Effect.provideService(Crypto.Crypto, crypto),
|
|
96
|
+
);
|
|
97
|
+
return yield* orchestration.dispatch(command);
|
|
98
|
+
});
|
|
99
|
+
const interruptThread = Effect.fn("T3ApplicationLive.interruptThread")(function* (
|
|
100
|
+
threadId: string,
|
|
101
|
+
) {
|
|
102
|
+
const snapshot = yield* orchestration.getThreadSnapshot(threadId);
|
|
103
|
+
const activeTurnId = snapshot.session?.activeTurnId ?? undefined;
|
|
104
|
+
const command = yield* makeThreadInterruptCommand({
|
|
105
|
+
threadId,
|
|
106
|
+
...(activeTurnId !== undefined ? { turnId: activeTurnId } : {}),
|
|
107
|
+
}).pipe(Effect.provideService(Crypto.Crypto, crypto));
|
|
108
|
+
return yield* orchestration.dispatch(command);
|
|
109
|
+
});
|
|
110
|
+
const deleteThread = Effect.fn("T3ApplicationLive.deleteThread")(function* (threadId: string) {
|
|
111
|
+
const snapshot = yield* loadThreadsSnapshot("all").pipe(
|
|
112
|
+
Effect.provideService(T3Orchestration, orchestration),
|
|
113
|
+
);
|
|
114
|
+
const thread = snapshot.threads.find((item) => item.id === threadId);
|
|
115
|
+
if (thread === undefined) {
|
|
116
|
+
return yield* Effect.fail(
|
|
117
|
+
new ThreadLookupError({
|
|
118
|
+
message: `thread not found: ${threadId}`,
|
|
119
|
+
threadId,
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (sessionNeedsStopBeforeDelete(thread.session)) {
|
|
124
|
+
const stopCommand = yield* makeThreadSessionStopCommand(threadId).pipe(
|
|
125
|
+
Effect.provideService(Crypto.Crypto, crypto),
|
|
126
|
+
);
|
|
127
|
+
yield* orchestration.dispatch(stopCommand);
|
|
128
|
+
}
|
|
129
|
+
const command = yield* makeThreadDeleteCommand(threadId).pipe(
|
|
130
|
+
Effect.provideService(Crypto.Crypto, crypto),
|
|
131
|
+
);
|
|
132
|
+
const dispatch = yield* orchestration.dispatch(command);
|
|
133
|
+
return { threadId, dispatch };
|
|
134
|
+
});
|
|
135
|
+
const updateThread: T3ThreadApplicationService["updateThread"] = (input) =>
|
|
136
|
+
makeUpdateThread()(input).pipe(
|
|
137
|
+
Effect.provideService(T3Orchestration, orchestration),
|
|
138
|
+
Effect.provideService(Crypto.Crypto, crypto),
|
|
139
|
+
);
|
|
64
140
|
const startThread = Effect.fn("T3ApplicationLive.startThread")(function* (
|
|
65
141
|
startInput: StartThreadInput,
|
|
66
142
|
policy?: {
|
|
@@ -96,20 +172,14 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
|
|
|
96
172
|
serverConfig,
|
|
97
173
|
}).pipe(Effect.provideService(Crypto.Crypto, crypto));
|
|
98
174
|
const createDispatch = yield* orchestration.dispatch(commands.createCommand);
|
|
99
|
-
yield*
|
|
100
|
-
orchestration,
|
|
101
|
-
sequence: createDispatch.sequence,
|
|
102
|
-
});
|
|
175
|
+
yield* awaitShellSequence(createDispatch.sequence);
|
|
103
176
|
const dispatch = yield* orchestration.dispatch(commands.turnCommand);
|
|
104
177
|
const threadId = commands.threadId;
|
|
105
178
|
const until = policy?.until ?? "dispatch";
|
|
106
179
|
if (until === "dispatch") {
|
|
107
180
|
return { dispatch, project: scope.project, threadId };
|
|
108
181
|
}
|
|
109
|
-
yield*
|
|
110
|
-
orchestration,
|
|
111
|
-
sequence: dispatch.sequence,
|
|
112
|
-
});
|
|
182
|
+
yield* awaitShellSequence(dispatch.sequence);
|
|
113
183
|
if (until === "visible") {
|
|
114
184
|
const thread = yield* Effect.scoped(
|
|
115
185
|
Effect.gen(function* () {
|
|
@@ -119,10 +189,7 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
|
|
|
119
189
|
);
|
|
120
190
|
return { dispatch, project: scope.project, threadId, thread };
|
|
121
191
|
}
|
|
122
|
-
const thread = yield*
|
|
123
|
-
orchestration,
|
|
124
|
-
threadId,
|
|
125
|
-
});
|
|
192
|
+
const thread = yield* awaitThreadCompletion(threadId);
|
|
126
193
|
yield* failIfThreadError(thread);
|
|
127
194
|
return { dispatch, project: scope.project, threadId, thread };
|
|
128
195
|
});
|
|
@@ -148,10 +215,7 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
|
|
|
148
215
|
if (until === "dispatch") {
|
|
149
216
|
return { dispatch, threadId: input.threadId };
|
|
150
217
|
}
|
|
151
|
-
yield*
|
|
152
|
-
orchestration,
|
|
153
|
-
sequence: dispatch.sequence,
|
|
154
|
-
});
|
|
218
|
+
yield* awaitShellSequence(dispatch.sequence);
|
|
155
219
|
if (until === "visible") {
|
|
156
220
|
const thread = yield* Effect.scoped(
|
|
157
221
|
Effect.gen(function* () {
|
|
@@ -161,33 +225,20 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
|
|
|
161
225
|
);
|
|
162
226
|
return { dispatch, threadId: input.threadId, thread };
|
|
163
227
|
}
|
|
164
|
-
const thread = yield*
|
|
165
|
-
orchestration,
|
|
166
|
-
threadId: input.threadId,
|
|
167
|
-
});
|
|
228
|
+
const thread = yield* awaitThreadCompletion(input.threadId);
|
|
168
229
|
yield* failIfThreadError(thread);
|
|
169
230
|
return { dispatch, threadId: input.threadId, thread };
|
|
170
231
|
});
|
|
171
|
-
const watchThread = (threadId: string) =>
|
|
172
|
-
watchThreadEvents({
|
|
173
|
-
orchestration,
|
|
174
|
-
threadId,
|
|
175
|
-
});
|
|
232
|
+
const watchThread = (threadId: string) => streamThreadEvents(threadId);
|
|
176
233
|
const waitForThread = Effect.fn("T3ApplicationLive.waitForThread")(function* (threadId: string) {
|
|
177
|
-
const thread = yield*
|
|
178
|
-
orchestration,
|
|
179
|
-
threadId,
|
|
180
|
-
});
|
|
234
|
+
const thread = yield* awaitThreadCompletion(threadId);
|
|
181
235
|
yield* failIfThreadError(thread);
|
|
182
236
|
return thread;
|
|
183
237
|
});
|
|
184
238
|
const callbackThread = Effect.fn("T3ApplicationLive.callbackThread")(function* (
|
|
185
239
|
input: CallbackThreadInput,
|
|
186
240
|
) {
|
|
187
|
-
yield*
|
|
188
|
-
orchestration,
|
|
189
|
-
threadId: input.fromThreadId,
|
|
190
|
-
});
|
|
241
|
+
yield* awaitThreadCompletion(input.fromThreadId);
|
|
191
242
|
const result = yield* sendThread(
|
|
192
243
|
{ threadId: input.targetThreadId, message: input.prompt },
|
|
193
244
|
{ until: "dispatch" },
|
|
@@ -220,6 +271,10 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
|
|
|
220
271
|
return {
|
|
221
272
|
approveThread,
|
|
222
273
|
archiveThread,
|
|
274
|
+
deleteThread,
|
|
275
|
+
interruptThread,
|
|
276
|
+
updateThread,
|
|
277
|
+
unarchiveThread,
|
|
223
278
|
listThreads,
|
|
224
279
|
getThreadMessages,
|
|
225
280
|
respondToThread,
|
|
@@ -229,9 +284,37 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
|
|
|
229
284
|
watchThread,
|
|
230
285
|
waitForThread,
|
|
231
286
|
callbackThread,
|
|
287
|
+
} satisfies T3ThreadApplicationService;
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const loadThreadsSnapshot = Effect.fn("loadThreadsSnapshot")(function* (
|
|
291
|
+
include: ListThreadsInclude,
|
|
292
|
+
) {
|
|
293
|
+
const orchestration = yield* T3Orchestration;
|
|
294
|
+
if (include === "active") {
|
|
295
|
+
return yield* orchestration.getShellSnapshot();
|
|
296
|
+
}
|
|
297
|
+
if (include === "archived") {
|
|
298
|
+
return yield* orchestration.getArchivedShellSnapshot();
|
|
299
|
+
}
|
|
300
|
+
const [activeSnapshot, archivedSnapshot] = yield* Effect.all([
|
|
301
|
+
orchestration.getShellSnapshot(),
|
|
302
|
+
orchestration.getArchivedShellSnapshot(),
|
|
303
|
+
]);
|
|
304
|
+
return {
|
|
305
|
+
...activeSnapshot,
|
|
306
|
+
threads: dedupeThreadsById([...activeSnapshot.threads, ...archivedSnapshot.threads]),
|
|
232
307
|
};
|
|
233
308
|
});
|
|
234
309
|
|
|
310
|
+
function dedupeThreadsById(threads: ReadonlyArray<OrchestrationThreadShell>) {
|
|
311
|
+
const byId = new Map<string, OrchestrationThreadShell>();
|
|
312
|
+
for (const thread of threads) {
|
|
313
|
+
byId.set(thread.id, thread);
|
|
314
|
+
}
|
|
315
|
+
return [...byId.values()];
|
|
316
|
+
}
|
|
317
|
+
|
|
235
318
|
export type ThreadShow = {
|
|
236
319
|
readonly id: string;
|
|
237
320
|
readonly projectId: string;
|
package/src/bin.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { createCliCommand } from "./cli/app.ts";
|
|
|
9
9
|
import { NodeEnvironmentLive } from "./environment/layer.ts";
|
|
10
10
|
import { T3InputLive } from "./cli/input/layer.ts";
|
|
11
11
|
import { T3OutputLive } from "./cli/output/layer.ts";
|
|
12
|
+
import { NodeTerminalIoLive } from "./cli/terminal/io-node-layer.ts";
|
|
12
13
|
import { T3Output } from "./cli/output/service.ts";
|
|
13
14
|
import { AppLayer } from "./runtime/layer.ts";
|
|
14
15
|
import { T3VersionBundledLive, T3VersionPackageJsonLive } from "./version/layer.ts";
|
|
@@ -27,6 +28,7 @@ const CliLayer = Layer.mergeAll(
|
|
|
27
28
|
NodeEnvironmentLive,
|
|
28
29
|
T3InputLive.pipe(Layer.provide(NodeServices.layer)),
|
|
29
30
|
T3OutputLive.pipe(Layer.provide(NodeServices.layer)),
|
|
31
|
+
NodeTerminalIoLive,
|
|
30
32
|
VersionLive.pipe(Layer.provide(NodeServices.layer)),
|
|
31
33
|
);
|
|
32
34
|
|
package/src/cli/app.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "effect/unstable/cli";
|
|
2
2
|
|
|
3
3
|
import { createAuthCommand } from "./auth.ts";
|
|
4
|
+
import { createTerminalCommandGroup } from "./terminal.ts";
|
|
4
5
|
import { createModelCommand } from "./model.ts";
|
|
5
6
|
import { createProjectCommand } from "./project.ts";
|
|
6
7
|
import { createThreadCommand } from "./thread.ts";
|
|
@@ -19,6 +20,7 @@ export function createCliCommand() {
|
|
|
19
20
|
listThreadsCommand,
|
|
20
21
|
createModelCommand(),
|
|
21
22
|
createProjectCommand(),
|
|
23
|
+
createTerminalCommandGroup(),
|
|
22
24
|
startThreadCommand,
|
|
23
25
|
sendThreadCommand,
|
|
24
26
|
showThreadCommand,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Prompt from "effect/unstable/cli/Prompt";
|
|
3
|
+
|
|
4
|
+
import { DestructiveConfirmationRequiredError } from "./error.ts";
|
|
5
|
+
import { isInteractiveHumanTerminal } from "./output-format.ts";
|
|
6
|
+
import type { EnvironmentShape } from "../environment/service.ts";
|
|
7
|
+
|
|
8
|
+
export const requireDestructiveConfirmation = Effect.fn("requireDestructiveConfirmation")(
|
|
9
|
+
function* (input: {
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly yes: boolean;
|
|
12
|
+
readonly environment: EnvironmentShape;
|
|
13
|
+
}) {
|
|
14
|
+
if (input.yes) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (!isInteractiveHumanTerminal(input.environment)) {
|
|
18
|
+
yield* Effect.fail(
|
|
19
|
+
new DestructiveConfirmationRequiredError({
|
|
20
|
+
message: "destructive action requires --yes in non-interactive mode",
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const confirmed = yield* Prompt.run(Prompt.confirm({ message: input.message, initial: false }));
|
|
26
|
+
if (confirmed) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
yield* Effect.fail(new DestructiveConfirmationRequiredError({ message: "aborted" }));
|
|
30
|
+
},
|
|
31
|
+
);
|