openshain 0.1.1 → 0.3.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/bin.d.ts +2 -0
- package/dist/bin.js +108 -0
- package/dist/commands/init.d.ts +21 -0
- package/dist/commands/init.js +128 -0
- package/dist/commands/mcp.d.ts +10 -0
- package/dist/commands/mcp.js +17 -0
- package/dist/commands/tools.d.ts +8 -0
- package/dist/commands/tools.js +18 -0
- package/dist/commands/work.d.ts +15 -0
- package/dist/commands/work.js +87 -0
- package/dist/format.d.ts +15 -0
- package/dist/format.js +103 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/labels.d.ts +13 -0
- package/dist/labels.js +59 -0
- package/dist/report.d.ts +6 -0
- package/dist/report.js +73 -0
- package/dist/tui/app.d.ts +9 -0
- package/dist/tui/app.js +191 -0
- package/dist/tui/banner.d.ts +15 -0
- package/dist/tui/banner.js +29 -0
- package/dist/tui/controller.d.ts +59 -0
- package/dist/tui/controller.js +325 -0
- package/dist/tui/index.d.ts +7 -0
- package/dist/tui/index.js +45 -0
- package/dist/tui/lines.d.ts +12 -0
- package/dist/tui/lines.js +68 -0
- package/dist/usage.d.ts +12 -0
- package/dist/usage.js +32 -0
- package/dist/workspace.d.ts +2 -0
- package/dist/workspace.js +20 -0
- package/package.json +17 -7
- package/src/bin.ts +5 -39
- package/src/commands/init.ts +9 -9
- package/src/commands/tools.ts +7 -2
- package/src/commands/work.ts +3 -34
- package/src/index.ts +1 -10
- package/src/labels.ts +2 -2
- package/src/{commands/run.ts → report.ts} +6 -59
- package/src/tui/controller.ts +113 -75
- package/src/tui/index.ts +3 -1
- package/src/usage.ts +1 -2
- package/src/workspace.ts +1 -1
package/src/commands/init.ts
CHANGED
|
@@ -52,15 +52,15 @@ export const MCP_TEMPLATE = `${JSON.stringify(
|
|
|
52
52
|
/** What an outside agent reads before working in the folder. Codex reads AGENTS.md; Claude Code reads it through CLAUDE.md. */
|
|
53
53
|
export const AGENTS_TEMPLATE = `# この会社フォルダで働くエージェントへ
|
|
54
54
|
|
|
55
|
-
このフォルダは openshain の Company Workspace です。この指示は、Claude Code や Codex のような外部のエージェントが MCP
|
|
55
|
+
このフォルダは openshain の Company Workspace です。この指示は、Claude Code や Codex のような外部のエージェントが MCP 経由でこのフォルダを扱うときのものです。openshain の対話型 CLI も同じ手順で Runtime を使います。
|
|
56
56
|
|
|
57
57
|
会社のファイルの読み書きと集計は openshain の MCP tool で行います。Claude Code や Codex 自身の Read、Write、Bash は会社のファイルには使いません。Runtime を通らなかった操作は記録に残らないためです。
|
|
58
58
|
|
|
59
59
|
- 依頼を受けたら、まず \`work_create\` に依頼の文をそのまま渡して Work を作る
|
|
60
|
-
- ファイルは \`fs_list\`、\`fs_search\`、\`fs_read\`、\`csv_read\`、\`markdown_read\` で見る。合計や件数は \`csv_aggregate\`
|
|
60
|
+
- ファイルは \`fs_list\`、\`fs_search\`、\`fs_read\`、\`csv_read\`、\`markdown_read\` で見る。合計や件数は \`csv_aggregate\` に任せ、自分で合計しない
|
|
61
61
|
- 書くときは \`fs_write\` か \`csv_write\`
|
|
62
62
|
- 終わったら \`work_complete\` に、何をしたかと、書いたファイルを渡す。続けられないときは \`work_fail\`
|
|
63
|
-
- \`openshain.yaml\` と \`work/\` は Runtime
|
|
63
|
+
- \`openshain.yaml\` と \`work/\` は Runtime のもの。変更しない
|
|
64
64
|
`;
|
|
65
65
|
|
|
66
66
|
export const CLAUDE_TEMPLATE = "@AGENTS.md\n";
|
|
@@ -106,11 +106,11 @@ export async function init({ workspaceRoot, write }: InitOptions): Promise<void>
|
|
|
106
106
|
} catch (err) {
|
|
107
107
|
if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
|
|
108
108
|
if (name === ".mcp.json") write(await addToMcpConfig(path));
|
|
109
|
-
else write(`${path}
|
|
109
|
+
else write(`${path} はすでにあるので変更しません。`);
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
write(
|
|
113
|
-
"company と principal を自分の会社に合わせ、api_key_env に書いた環境変数を設定してから openshain
|
|
113
|
+
"company と principal を自分の会社に合わせ、api_key_env に書いた環境変数を設定してから openshain を実行してください。",
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -123,10 +123,10 @@ async function addToMcpConfig(path: string): Promise<string> {
|
|
|
123
123
|
try {
|
|
124
124
|
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
125
125
|
} catch {
|
|
126
|
-
return `${path} はすでにあり、JSON
|
|
126
|
+
return `${path} はすでにあり、JSON として読めないので変更しません。openshain を手動で登録してください。`;
|
|
127
127
|
}
|
|
128
128
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
129
|
-
return `${path}
|
|
129
|
+
return `${path} はすでにあり、形が違うので変更しません。openshain を手動で登録してください。`;
|
|
130
130
|
}
|
|
131
131
|
const config = parsed as { mcpServers?: unknown };
|
|
132
132
|
const servers =
|
|
@@ -134,11 +134,11 @@ async function addToMcpConfig(path: string): Promise<string> {
|
|
|
134
134
|
? (config.mcpServers as Record<string, unknown>)
|
|
135
135
|
: {};
|
|
136
136
|
if (Object.hasOwn(servers, "openshain"))
|
|
137
|
-
return `${path} には openshain
|
|
137
|
+
return `${path} には openshain がすでにあるので変更しません。`;
|
|
138
138
|
const next = {
|
|
139
139
|
...config,
|
|
140
140
|
mcpServers: { ...servers, openshain: { command: "openshain", args: ["mcp"] } },
|
|
141
141
|
};
|
|
142
142
|
await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
143
|
-
return `${path} に openshain
|
|
143
|
+
return `${path} に openshain を追加しました。他の項目はそのままです。`;
|
|
144
144
|
}
|
package/src/commands/tools.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
ASK_USER,
|
|
3
|
+
createToolRegistry,
|
|
4
|
+
loadConfig,
|
|
5
|
+
RUNTIME_PROVIDER_ID,
|
|
6
|
+
type RuntimeProviders,
|
|
7
|
+
} from "@openshain/core";
|
|
3
8
|
|
|
4
9
|
export interface ToolsListOptions {
|
|
5
10
|
workspaceRoot: string;
|
package/src/commands/work.ts
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
import { pendingQuestions } from "@openshain/agent";
|
|
2
1
|
import {
|
|
3
2
|
type AnyEvent,
|
|
4
|
-
createRuntime,
|
|
5
3
|
type Event,
|
|
6
|
-
isTerminal,
|
|
7
4
|
parseWorkId,
|
|
8
|
-
|
|
9
|
-
SESSION_WORK_TYPE,
|
|
5
|
+
pendingQuestions,
|
|
10
6
|
type Work,
|
|
11
7
|
WorkStore,
|
|
12
8
|
} from "@openshain/core";
|
|
13
9
|
import { describeInput, padDisplay } from "../format.ts";
|
|
14
10
|
import { errorLabel, failureLabel, rejectionLabel, statusLabel } from "../labels.ts";
|
|
11
|
+
import { nextActor } from "../report.ts";
|
|
15
12
|
import { formatUsage, summarizeUsage } from "../usage.ts";
|
|
16
|
-
import { type DriveOptions, drive, nextActor } from "./run.ts";
|
|
17
13
|
|
|
18
14
|
export interface WorkListOptions {
|
|
19
15
|
workspaceRoot: string;
|
|
@@ -24,7 +20,7 @@ export interface WorkListOptions {
|
|
|
24
20
|
export async function workList({ workspaceRoot, write }: WorkListOptions): Promise<void> {
|
|
25
21
|
const { works, problems } = await new WorkStore(workspaceRoot).list();
|
|
26
22
|
if (works.length === 0 && problems.length === 0) {
|
|
27
|
-
write(
|
|
23
|
+
write("Work はまだありません。openshain で社員エージェントに依頼すると始まります。");
|
|
28
24
|
return;
|
|
29
25
|
}
|
|
30
26
|
for (const work of works) {
|
|
@@ -86,33 +82,6 @@ export function describeWork(work: Work, events: AnyEvent[]): string[] {
|
|
|
86
82
|
return lines;
|
|
87
83
|
}
|
|
88
84
|
|
|
89
|
-
export interface WorkResumeOptions extends DriveOptions {
|
|
90
|
-
workspaceRoot: string;
|
|
91
|
-
providers: RuntimeProviders;
|
|
92
|
-
id: string;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Continues a work that stopped before its end, answering its questions when the caller can. */
|
|
96
|
-
export async function workResume(options: WorkResumeOptions): Promise<number> {
|
|
97
|
-
const workId = parseWorkId(options.id);
|
|
98
|
-
const work = await new WorkStore(options.workspaceRoot).get(workId);
|
|
99
|
-
if (work.type === SESSION_WORK_TYPE) {
|
|
100
|
-
options.write(
|
|
101
|
-
`${work.id} は会話の記録のため、再開できません。会話は openshain で始め直してください。`,
|
|
102
|
-
);
|
|
103
|
-
return 1;
|
|
104
|
-
}
|
|
105
|
-
if (isTerminal(work.status)) {
|
|
106
|
-
options.write(`${work.id} は${statusLabel(work.status)}のため、再開できません。`);
|
|
107
|
-
return 1;
|
|
108
|
-
}
|
|
109
|
-
const runtime = await createRuntime({
|
|
110
|
-
workspaceRoot: options.workspaceRoot,
|
|
111
|
-
providers: options.providers,
|
|
112
|
-
});
|
|
113
|
-
return drive(runtime, workId, options);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
85
|
/** One line per tool call, in log order, with its outcome when it was rejected or failed. */
|
|
117
86
|
function toolLines(events: AnyEvent[]): string[] {
|
|
118
87
|
const lines = new Map<string, string>();
|
package/src/index.ts
CHANGED
|
@@ -8,22 +8,12 @@ export {
|
|
|
8
8
|
MCP_TEMPLATE,
|
|
9
9
|
} from "./commands/init.ts";
|
|
10
10
|
export { type McpOptions, mcp } from "./commands/mcp.ts";
|
|
11
|
-
export {
|
|
12
|
-
type DriveOptions,
|
|
13
|
-
drive,
|
|
14
|
-
nextActor,
|
|
15
|
-
type RunOptions,
|
|
16
|
-
report,
|
|
17
|
-
run,
|
|
18
|
-
} from "./commands/run.ts";
|
|
19
11
|
export { type ToolsListOptions, toolsList } from "./commands/tools.ts";
|
|
20
12
|
export {
|
|
21
13
|
describeWork,
|
|
22
14
|
type WorkListOptions,
|
|
23
|
-
type WorkResumeOptions,
|
|
24
15
|
type WorkShowOptions,
|
|
25
16
|
workList,
|
|
26
|
-
workResume,
|
|
27
17
|
workShow,
|
|
28
18
|
} from "./commands/work.ts";
|
|
29
19
|
export {
|
|
@@ -36,5 +26,6 @@ export {
|
|
|
36
26
|
STATUS_LABELS,
|
|
37
27
|
statusLabel,
|
|
38
28
|
} from "./labels.ts";
|
|
29
|
+
export { nextActor, progressLine, report } from "./report.ts";
|
|
39
30
|
export { formatUsage, summarizeUsage, type UsageSummary } from "./usage.ts";
|
|
40
31
|
export { findWorkspace } from "./workspace.ts";
|
package/src/labels.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { FailureReason } from "@openshain/
|
|
2
|
-
import type { ErrorCode, ToolRejectionCode, WorkStatus } from "@openshain/core";
|
|
1
|
+
import type { ErrorCode, FailureReason, ToolRejectionCode, WorkStatus } from "@openshain/core";
|
|
3
2
|
|
|
4
3
|
/** The words shown for a work's status. The log keeps the original value. */
|
|
5
4
|
export const STATUS_LABELS: Record<WorkStatus, string> = {
|
|
@@ -28,6 +27,7 @@ export const REJECTION_LABELS: Record<ToolRejectionCode, string> = {
|
|
|
28
27
|
reserved_path: "予約されたパス",
|
|
29
28
|
outside_workspace: "workspace の外",
|
|
30
29
|
invalid_path: "不正なパス",
|
|
30
|
+
limit_reached: "Tool 呼び出しの上限",
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
/** A heading for a runtime error, before the original message. */
|
|
@@ -1,66 +1,13 @@
|
|
|
1
|
-
import { pendingQuestions, runWork } from "@openshain/agent";
|
|
2
1
|
import {
|
|
3
2
|
type AnyEvent,
|
|
4
|
-
createRuntime,
|
|
5
3
|
type Event,
|
|
6
|
-
|
|
7
|
-
type RuntimeProviders,
|
|
4
|
+
pendingQuestions,
|
|
8
5
|
type ToolContent,
|
|
9
6
|
type Work,
|
|
10
|
-
type WorkId,
|
|
11
7
|
} from "@openshain/core";
|
|
12
|
-
import { describeInput, truncate } from "
|
|
13
|
-
import { failureLabel, rejectionLabel, statusLabel } from "
|
|
14
|
-
import { formatUsage, summarizeUsage } from "
|
|
15
|
-
|
|
16
|
-
export interface DriveOptions {
|
|
17
|
-
write: (line: string) => void;
|
|
18
|
-
/** Answers the model's questions. Without it, a question leaves the work waiting and the run ends. */
|
|
19
|
-
ask?: (question: string) => Promise<string>;
|
|
20
|
-
/** Stops the run. The work stays where it is and can be resumed. */
|
|
21
|
-
signal?: AbortSignal;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface RunOptions extends DriveOptions {
|
|
25
|
-
workspaceRoot: string;
|
|
26
|
-
providers: RuntimeProviders;
|
|
27
|
-
objective: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Creates a work for the request and drives it. Exit code 0 when the work completed. */
|
|
31
|
-
export async function run(options: RunOptions): Promise<number> {
|
|
32
|
-
const runtime = await createRuntime({
|
|
33
|
-
workspaceRoot: options.workspaceRoot,
|
|
34
|
-
providers: options.providers,
|
|
35
|
-
});
|
|
36
|
-
const work = await runtime.works.create({
|
|
37
|
-
objective: options.objective,
|
|
38
|
-
principal: runtime.config.principal.id,
|
|
39
|
-
profession: runtime.config.profession.id,
|
|
40
|
-
});
|
|
41
|
-
options.write(`${work.id} を開始`);
|
|
42
|
-
return drive(runtime, work.id, options);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Drives a work from its current state, printing one line per tool call, and closes with the report. */
|
|
46
|
-
export async function drive(
|
|
47
|
-
runtime: Runtime,
|
|
48
|
-
workId: WorkId,
|
|
49
|
-
options: DriveOptions,
|
|
50
|
-
): Promise<number> {
|
|
51
|
-
const names = new Map<string, string>();
|
|
52
|
-
const done = await runWork(runtime, workId, {
|
|
53
|
-
...(options.ask && { onInput: options.ask }),
|
|
54
|
-
...(options.signal && { signal: options.signal }),
|
|
55
|
-
onEvent: (event) => {
|
|
56
|
-
const line = progressLine(event, names);
|
|
57
|
-
if (line) options.write(line);
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
const events = await runtime.works.events(workId);
|
|
61
|
-
for (const line of report(done, events)) options.write(line);
|
|
62
|
-
return done.status === "completed" ? 0 : 1;
|
|
63
|
-
}
|
|
8
|
+
import { describeInput, truncate } from "./format.ts";
|
|
9
|
+
import { failureLabel, rejectionLabel, statusLabel } from "./labels.ts";
|
|
10
|
+
import { formatUsage, summarizeUsage } from "./usage.ts";
|
|
64
11
|
|
|
65
12
|
/** One line for a tool call, a rejection or a failure; nothing for the other events. `names` maps call ids to tool names. */
|
|
66
13
|
export function progressLine(event: AnyEvent, names: Map<string, string>): string | undefined {
|
|
@@ -126,11 +73,11 @@ export function nextActor(work: Work): string {
|
|
|
126
73
|
case "cancelled":
|
|
127
74
|
return "次に動く人はいません。";
|
|
128
75
|
case "waiting_input":
|
|
129
|
-
return `次は利用者の番です。openshain work resume ${work.id}
|
|
76
|
+
return `次は利用者の番です。openshain の会話で /work resume ${work.id} を実行し、続きを依頼すると質問に答えられます。`;
|
|
130
77
|
case "waiting_approval":
|
|
131
78
|
return "次は利用者の番です。承認が要ります。";
|
|
132
79
|
case "failed":
|
|
133
|
-
return "
|
|
80
|
+
return "次は利用者の番です。原因を修正して、もう一度依頼してください。";
|
|
134
81
|
default:
|
|
135
82
|
return "次は model の番です。";
|
|
136
83
|
}
|
package/src/tui/controller.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { createSession, type Session, type TurnResult } from "@openshain/agent";
|
|
1
|
+
import { connectInMemory, createSession, type Session, type TurnResult } from "@openshain/agent";
|
|
2
2
|
import {
|
|
3
|
-
type AnyEvent,
|
|
4
|
-
createRuntime,
|
|
5
3
|
type Event,
|
|
6
|
-
|
|
4
|
+
loadConfig,
|
|
5
|
+
OpenshainError,
|
|
7
6
|
type RuntimeProviders,
|
|
8
7
|
type WorkId,
|
|
8
|
+
WorkStore,
|
|
9
9
|
} from "@openshain/core";
|
|
10
|
-
import {
|
|
10
|
+
import { createMcpServer } from "@openshain/mcp";
|
|
11
11
|
import { toolsList } from "../commands/tools.ts";
|
|
12
|
-
import { workList,
|
|
12
|
+
import { workList, workShow } from "../commands/work.ts";
|
|
13
13
|
import { plain } from "../format.ts";
|
|
14
14
|
import { statusLabel } from "../labels.ts";
|
|
15
|
+
import { progressLine, report } from "../report.ts";
|
|
15
16
|
import { LOGO_ROWS, VERSION } from "./banner.ts";
|
|
16
17
|
|
|
17
18
|
/** logo and banner are the rows shown once when the screen opens: the wordmark, the version, the folder. */
|
|
@@ -66,13 +67,12 @@ export interface Controller {
|
|
|
66
67
|
export interface ControllerOptions {
|
|
67
68
|
workspaceRoot: string;
|
|
68
69
|
providers: RuntimeProviders;
|
|
69
|
-
runtime?: Runtime;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const HELP = [
|
|
73
73
|
"/work list Work の一覧",
|
|
74
74
|
"/work show <id> Work の詳細",
|
|
75
|
-
"/work resume <id> 止まった Work
|
|
75
|
+
"/work resume <id> 止まった Work を候補にする。次の依頼がそれに沿えば続ける",
|
|
76
76
|
"/tools 使える Tool",
|
|
77
77
|
"/quit 終わる",
|
|
78
78
|
"↑ ↓ 前に送った行を入力欄に呼び戻す。いちばん下は新しい入力",
|
|
@@ -85,11 +85,36 @@ const HELP = [
|
|
|
85
85
|
const QUESTION_WITHDRAWN =
|
|
86
86
|
"the person stopped the work while it waited for their answer; the question is still pending and the work can be resumed";
|
|
87
87
|
|
|
88
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* The state behind the screen: a session, the works it starts, and the lines to show. The
|
|
90
|
+
* conversation reaches the runtime only as an MCP client of the workspace's own server, the way
|
|
91
|
+
* any other agent does; the records are read directly for the closing lines.
|
|
92
|
+
*/
|
|
89
93
|
export async function createController(options: ControllerOptions): Promise<Controller> {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
const { workspaceRoot, providers } = options;
|
|
95
|
+
const config = await loadConfig(workspaceRoot, { modelProviders: Object.keys(providers.models) });
|
|
96
|
+
if (!config.model) {
|
|
97
|
+
throw new OpenshainError(
|
|
98
|
+
"config",
|
|
99
|
+
"対話にはモデルが要ります。openshain.yaml に model を書いてください。Claude Code や Codex から使うだけなら要りません。",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const modelFactory = Object.hasOwn(providers.models, config.model.provider)
|
|
103
|
+
? providers.models[config.model.provider]
|
|
104
|
+
: undefined;
|
|
105
|
+
if (!modelFactory) {
|
|
106
|
+
throw new OpenshainError("config", `unknown model provider "${config.model.provider}"`);
|
|
107
|
+
}
|
|
108
|
+
const model = modelFactory(config.model);
|
|
109
|
+
if (!model.describe().capabilities.tools) {
|
|
110
|
+
throw new OpenshainError(
|
|
111
|
+
"config",
|
|
112
|
+
`model ${config.model.provider}/${config.model.model} cannot call tools; openshain needs a model with tool support`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const server = await createMcpServer({ workspaceRoot, tools: providers.tools });
|
|
116
|
+
const client = await connectInMemory(server);
|
|
117
|
+
const store = new WorkStore(workspaceRoot);
|
|
93
118
|
const listeners = new Set<() => void>();
|
|
94
119
|
let nextId = 1;
|
|
95
120
|
const state: ControllerState = {
|
|
@@ -97,8 +122,8 @@ export async function createController(options: ControllerOptions): Promise<Cont
|
|
|
97
122
|
busy: false,
|
|
98
123
|
closed: false,
|
|
99
124
|
status: {
|
|
100
|
-
company:
|
|
101
|
-
model: `${
|
|
125
|
+
company: config.company.name,
|
|
126
|
+
model: `${config.model.provider}/${config.model.model}`,
|
|
102
127
|
usage: { modelCalls: 0, inputTokens: 0, outputTokens: 0 },
|
|
103
128
|
},
|
|
104
129
|
};
|
|
@@ -129,8 +154,6 @@ export async function createController(options: ControllerOptions): Promise<Cont
|
|
|
129
154
|
let aborter: AbortController | undefined;
|
|
130
155
|
let running: Promise<void> | undefined;
|
|
131
156
|
let closing: Promise<void> | undefined;
|
|
132
|
-
/** The child work of the current turn while it is unfinished. */
|
|
133
|
-
let lastWorkId: WorkId | undefined;
|
|
134
157
|
const names = new Map<string, string>();
|
|
135
158
|
|
|
136
159
|
const ask = (workId: WorkId, question: string): Promise<string> => {
|
|
@@ -155,60 +178,75 @@ export async function createController(options: ControllerOptions): Promise<Cont
|
|
|
155
178
|
|
|
156
179
|
/** The lines the CLI prints when a work ends, shown among the progress lines. */
|
|
157
180
|
const closingLines = async (workId: WorkId) => {
|
|
158
|
-
for (const line of await workReport(
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const onWorkEvent = (workId: WorkId, event: AnyEvent): void | Promise<void> => {
|
|
162
|
-
lastWorkId = workId;
|
|
163
|
-
if (event.type === "work.status_changed") {
|
|
164
|
-
state.status.work = {
|
|
165
|
-
id: workId,
|
|
166
|
-
status: (event as Event<"work.status_changed">).payload.to,
|
|
167
|
-
};
|
|
168
|
-
} else if (event.type === "work.completed" || event.type === "work.failed") {
|
|
169
|
-
lastWorkId = undefined;
|
|
170
|
-
state.status.work = {
|
|
171
|
-
id: workId,
|
|
172
|
-
status: event.type === "work.completed" ? "completed" : "failed",
|
|
173
|
-
};
|
|
174
|
-
return closingLines(workId);
|
|
175
|
-
}
|
|
176
|
-
const line = progressLine(event, names);
|
|
177
|
-
if (line) push("progress", line);
|
|
178
|
-
else notify();
|
|
181
|
+
for (const line of await workReport(store, workId)) push("progress", line.trimStart());
|
|
179
182
|
};
|
|
180
183
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
let sessionId: WorkId | undefined;
|
|
185
|
+
const session: Session = await createSession(client, {
|
|
186
|
+
model,
|
|
187
|
+
config,
|
|
188
|
+
onEvent: (workId, event) => {
|
|
189
|
+
if (workId === sessionId) {
|
|
190
|
+
if (event.type === "usage.recorded") {
|
|
191
|
+
const { payload } = event as Event<"usage.recorded">;
|
|
192
|
+
if (payload.kind === "model_inference") {
|
|
193
|
+
state.status.usage.modelCalls += 1;
|
|
194
|
+
state.status.usage.inputTokens += payload.usage.inputTokens;
|
|
195
|
+
state.status.usage.outputTokens += payload.usage.outputTokens;
|
|
196
|
+
notify();
|
|
197
|
+
}
|
|
190
198
|
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (event.type === "work.status_changed") {
|
|
202
|
+
state.status.work = {
|
|
203
|
+
id: workId,
|
|
204
|
+
status: (event as Event<"work.status_changed">).payload.to,
|
|
205
|
+
};
|
|
206
|
+
notify();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (event.type === "work.completed" || event.type === "work.failed") {
|
|
210
|
+
state.status.work = {
|
|
211
|
+
id: workId,
|
|
212
|
+
status: event.type === "work.completed" ? "completed" : "failed",
|
|
213
|
+
};
|
|
214
|
+
return closingLines(workId);
|
|
191
215
|
}
|
|
216
|
+
// The work_* calls are the loop's own bookkeeping; the closing lines already say the work ended.
|
|
217
|
+
if (
|
|
218
|
+
event.type === "tool.called" &&
|
|
219
|
+
(event as Event<"tool.called">).payload.name.startsWith("work_")
|
|
220
|
+
) {
|
|
221
|
+
names.set(
|
|
222
|
+
(event as Event<"tool.called">).payload.callId,
|
|
223
|
+
(event as Event<"tool.called">).payload.name,
|
|
224
|
+
);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const line = progressLine(event, names);
|
|
228
|
+
if (line) push("progress", line);
|
|
229
|
+
else notify();
|
|
192
230
|
},
|
|
193
|
-
onWorkEvent,
|
|
194
231
|
onInput: ask,
|
|
195
232
|
});
|
|
233
|
+
sessionId = session.id;
|
|
196
234
|
state.status.agentName = session.agentName;
|
|
197
235
|
for (const row of LOGO_ROWS) push("logo", row);
|
|
198
236
|
push("banner", `openshain ${VERSION}`);
|
|
199
|
-
push("banner",
|
|
237
|
+
push("banner", workspaceRoot);
|
|
200
238
|
|
|
201
|
-
const stopped = (workId:
|
|
239
|
+
const stopped = (workId: WorkId | undefined) =>
|
|
202
240
|
workId
|
|
203
|
-
? `止めました。${workId} は途中のまま残っています。/work resume ${workId}
|
|
241
|
+
? `止めました。${workId} は途中のまま残っています。/work resume ${workId} で続けられるようにします。`
|
|
204
242
|
: "止めました。";
|
|
205
243
|
|
|
206
244
|
const explain = (result: TurnResult) => {
|
|
207
245
|
switch (result.stopped) {
|
|
208
246
|
case "turn_limit":
|
|
209
|
-
return "社員エージェントが 1
|
|
247
|
+
return "社員エージェントが 1 回の返答でできる回数を超えたので、ここで止めました。続きは改めて依頼してください。";
|
|
210
248
|
case "aborted":
|
|
211
|
-
return stopped(
|
|
249
|
+
return stopped(result.work);
|
|
212
250
|
case "max_tokens":
|
|
213
251
|
return "返答が長さの上限で切れました。";
|
|
214
252
|
case "refusal":
|
|
@@ -261,28 +299,22 @@ export async function createController(options: ControllerOptions): Promise<Cont
|
|
|
261
299
|
else if (name === "work" && sub === "show" && id)
|
|
262
300
|
await capture((write) => workShow({ workspaceRoot: options.workspaceRoot, id, write }));
|
|
263
301
|
else if (name === "work" && sub === "resume" && id) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
});
|
|
274
|
-
} catch (err) {
|
|
275
|
-
if (!signal.aborted) push("notice", message(err));
|
|
276
|
-
}
|
|
277
|
-
if (signal.aborted) push("notice", stopped(id));
|
|
278
|
-
});
|
|
302
|
+
try {
|
|
303
|
+
const work = await session.select(id as WorkId);
|
|
304
|
+
push(
|
|
305
|
+
"notice",
|
|
306
|
+
`${work.id}(${statusLabel(work.status)}、${work.objective})を候補にしました。次の依頼がこの Work に沿えば続けます。`,
|
|
307
|
+
);
|
|
308
|
+
} catch (err) {
|
|
309
|
+
push("notice", message(err));
|
|
310
|
+
}
|
|
279
311
|
} else if (name === "resume") {
|
|
280
312
|
push(
|
|
281
313
|
"notice",
|
|
282
314
|
"セッションの再開はまだありません。止まった Work を続けるなら /work resume <id> です。",
|
|
283
315
|
);
|
|
284
316
|
} else {
|
|
285
|
-
push("notice", `分からないコマンドです。/help
|
|
317
|
+
push("notice", `分からないコマンドです。/help で一覧が表示されます。`);
|
|
286
318
|
}
|
|
287
319
|
};
|
|
288
320
|
|
|
@@ -291,9 +323,15 @@ export async function createController(options: ControllerOptions): Promise<Cont
|
|
|
291
323
|
aborter?.abort();
|
|
292
324
|
settleQuestion();
|
|
293
325
|
await running;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
326
|
+
try {
|
|
327
|
+
await session.close();
|
|
328
|
+
} catch (err) {
|
|
329
|
+
push("notice", `会話の記録を閉じられませんでした。${message(err)}`);
|
|
330
|
+
} finally {
|
|
331
|
+
await client.close().catch(() => undefined);
|
|
332
|
+
state.closed = true;
|
|
333
|
+
notify();
|
|
334
|
+
}
|
|
297
335
|
})();
|
|
298
336
|
return closing;
|
|
299
337
|
}
|
|
@@ -346,10 +384,10 @@ export async function createController(options: ControllerOptions): Promise<Cont
|
|
|
346
384
|
};
|
|
347
385
|
}
|
|
348
386
|
|
|
349
|
-
/** Lines that close a work in the screen: the CLI's closing lines without the summary, which the
|
|
350
|
-
export async function workReport(
|
|
351
|
-
const work = await
|
|
352
|
-
const events = await
|
|
387
|
+
/** Lines that close a work in the screen: the CLI's closing lines without the summary, which the agent relays. */
|
|
388
|
+
export async function workReport(store: WorkStore, workId: WorkId): Promise<string[]> {
|
|
389
|
+
const work = await store.get(workId);
|
|
390
|
+
const events = await store.events(workId);
|
|
353
391
|
const lines = report(work, events);
|
|
354
392
|
return work.status === "completed" ? ["完了。", ...lines.slice(1)] : lines;
|
|
355
393
|
}
|
package/src/tui/index.ts
CHANGED
|
@@ -46,6 +46,8 @@ export async function startTui(options: TuiOptions): Promise<number> {
|
|
|
46
46
|
} finally {
|
|
47
47
|
leave();
|
|
48
48
|
}
|
|
49
|
-
console.log(
|
|
49
|
+
console.log(
|
|
50
|
+
`会話を終えました。記録は openshain work show ${controller.sessionId} で参照します。`,
|
|
51
|
+
);
|
|
50
52
|
return 0;
|
|
51
53
|
}
|
package/src/usage.ts
CHANGED
package/src/workspace.ts
CHANGED
|
@@ -14,7 +14,7 @@ export async function findWorkspace(start: string): Promise<string> {
|
|
|
14
14
|
if (parent === dir) {
|
|
15
15
|
throw new OpenshainError(
|
|
16
16
|
"config",
|
|
17
|
-
`${CONFIG_FILE_NAME} が見つかりません。${resolve(start)} から上に向かって探しました。openshain init
|
|
17
|
+
`${CONFIG_FILE_NAME} が見つかりません。${resolve(start)} から上に向かって探しました。openshain init で作成します。`,
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
dir = parent;
|