@openshain/core 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/config/load.d.ts +10 -0
- package/dist/config/load.js +66 -0
- package/dist/config/schema.d.ts +87 -0
- package/dist/config/schema.js +100 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +30 -0
- package/dist/ids.d.ts +11 -0
- package/dist/ids.js +21 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +22 -0
- package/dist/model/types.d.ts +57 -0
- package/dist/model/types.js +0 -0
- package/dist/runtime.d.ts +46 -0
- package/dist/runtime.js +147 -0
- package/dist/schemas.d.ts +9 -0
- package/dist/schemas.js +44 -0
- package/dist/tool/ask-user.d.ts +5 -0
- package/dist/tool/ask-user.js +22 -0
- package/dist/tool/load-module.d.ts +6 -0
- package/dist/tool/load-module.js +42 -0
- package/dist/tool/paths.d.ts +17 -0
- package/dist/tool/paths.js +82 -0
- package/dist/tool/registry.d.ts +30 -0
- package/dist/tool/registry.js +68 -0
- package/dist/tool/types.d.ts +44 -0
- package/dist/tool/types.js +17 -0
- package/dist/tool/validate.d.ts +14 -0
- package/dist/tool/validate.js +68 -0
- package/dist/uuid.d.ts +1 -0
- package/dist/uuid.js +33 -0
- package/dist/work/artifacts.d.ts +7 -0
- package/dist/work/artifacts.js +20 -0
- package/dist/work/event-log.d.ts +28 -0
- package/dist/work/event-log.js +140 -0
- package/dist/work/events.d.ts +329 -0
- package/dist/work/events.js +360 -0
- package/dist/work/history.d.ts +38 -0
- package/dist/work/history.js +70 -0
- package/dist/work/lock.d.ts +13 -0
- package/dist/work/lock.js +80 -0
- package/dist/work/projection.d.ts +31 -0
- package/dist/work/projection.js +133 -0
- package/dist/work/store.d.ts +58 -0
- package/dist/work/store.js +174 -0
- package/dist/work/work.d.ts +86 -0
- package/dist/work/work.js +149 -0
- package/package.json +15 -4
- package/src/config/load.ts +1 -1
- package/src/config/schema.ts +41 -31
- package/src/ids.ts +3 -2
- package/src/index.ts +14 -1
- package/src/runtime.ts +8 -2
- package/src/tool/ask-user.ts +25 -0
- package/src/tool/types.ts +2 -0
- package/src/uuid.ts +33 -0
- package/src/work/events.ts +20 -0
- package/src/work/history.ts +96 -0
- package/src/work/projection.ts +4 -1
package/src/config/schema.ts
CHANGED
|
@@ -46,23 +46,27 @@ export const ConfigFileSchema = z.strictObject({
|
|
|
46
46
|
}),
|
|
47
47
|
principal: z.strictObject({ id: identifier, name: z.string().min(1).max(200) }),
|
|
48
48
|
profession: z.strictObject({ id: identifier, instructions: z.string().min(1).max(100_000) }),
|
|
49
|
-
model: z
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
49
|
+
model: z
|
|
50
|
+
.strictObject({
|
|
51
|
+
provider: identifier,
|
|
52
|
+
model: z.string().min(1).max(200),
|
|
53
|
+
api_key_env: envVarName,
|
|
54
|
+
base_url: z
|
|
55
|
+
.url()
|
|
56
|
+
.refine((value) => {
|
|
57
|
+
const url = new URL(value);
|
|
58
|
+
return url.username === "" && url.password === "";
|
|
59
|
+
}, "base_url must not carry credentials; use api_key_env")
|
|
60
|
+
.refine((value) => {
|
|
61
|
+
const url = new URL(value);
|
|
62
|
+
return (
|
|
63
|
+
url.protocol === "https:" || (url.protocol === "http:" && isLoopback(url.hostname))
|
|
64
|
+
);
|
|
65
|
+
}, "base_url must use https unless it points at this machine (localhost, 127.0.0.0/8, ::1)")
|
|
66
|
+
.optional(),
|
|
67
|
+
options: z.record(z.string(), z.unknown()).optional(),
|
|
68
|
+
})
|
|
69
|
+
.optional(),
|
|
66
70
|
tools: z.array(toolProviderRef).default([{ provider: "standard" }]),
|
|
67
71
|
limits: z
|
|
68
72
|
.strictObject({
|
|
@@ -81,18 +85,22 @@ export type ToolProviderRef =
|
|
|
81
85
|
| { module: string; allow: readonly string[] | undefined };
|
|
82
86
|
|
|
83
87
|
/** Configuration as used in code (camelCase). */
|
|
88
|
+
/** The model section of openshain.yaml, as the model providers take it. */
|
|
89
|
+
export interface ModelConfig {
|
|
90
|
+
provider: string;
|
|
91
|
+
model: string;
|
|
92
|
+
apiKeyEnv: string;
|
|
93
|
+
baseUrl: string | undefined;
|
|
94
|
+
options: Record<string, unknown> | undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
84
97
|
export interface Config {
|
|
85
98
|
version: 1;
|
|
86
99
|
company: { name: string; language: Language };
|
|
87
100
|
principal: { id: string; name: string };
|
|
88
101
|
profession: { id: string; instructions: string };
|
|
89
|
-
model
|
|
90
|
-
|
|
91
|
-
model: string;
|
|
92
|
-
apiKeyEnv: string;
|
|
93
|
-
baseUrl: string | undefined;
|
|
94
|
-
options: Record<string, unknown> | undefined;
|
|
95
|
-
};
|
|
102
|
+
/** The model the interactive CLI runs on. Absent when the workspace is used from other agents only. */
|
|
103
|
+
model?: ModelConfig;
|
|
96
104
|
tools: ToolProviderRef[];
|
|
97
105
|
limits: { maxModelCalls: number; maxToolCalls: number; maxOutputTokens: number };
|
|
98
106
|
debug: { persistRaw: boolean };
|
|
@@ -104,13 +112,15 @@ export function toConfig(file: ConfigFile): Config {
|
|
|
104
112
|
company: { name: file.company.name, language: file.company.language },
|
|
105
113
|
principal: { id: file.principal.id, name: file.principal.name },
|
|
106
114
|
profession: { id: file.profession.id, instructions: file.profession.instructions },
|
|
107
|
-
model
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
...(file.model && {
|
|
116
|
+
model: {
|
|
117
|
+
provider: file.model.provider,
|
|
118
|
+
model: file.model.model,
|
|
119
|
+
apiKeyEnv: file.model.api_key_env,
|
|
120
|
+
baseUrl: file.model.base_url,
|
|
121
|
+
options: file.model.options,
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
114
124
|
tools: file.tools.map(toToolProviderRef),
|
|
115
125
|
limits: {
|
|
116
126
|
maxModelCalls: file.limits.max_model_calls,
|
package/src/ids.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OpenshainError } from "./errors.ts";
|
|
2
|
+
import { uuidv7 } from "./uuid.ts";
|
|
2
3
|
|
|
3
4
|
declare const brand: unique symbol;
|
|
4
5
|
type Brand<T, Name extends string> = T & { readonly [brand]: Name };
|
|
@@ -9,11 +10,11 @@ export type EventId = Brand<string, "EventId">;
|
|
|
9
10
|
const UUID_V7 = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
10
11
|
|
|
11
12
|
export function newWorkId(): WorkId {
|
|
12
|
-
return `work_${
|
|
13
|
+
return `work_${uuidv7()}` as WorkId;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function newEventId(): EventId {
|
|
16
|
-
return `evt_${
|
|
17
|
+
return `evt_${uuidv7()}` as EventId;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function parseWorkId(value: string): WorkId {
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ export {
|
|
|
5
5
|
type ParseConfigOptions,
|
|
6
6
|
parseConfig,
|
|
7
7
|
} from "./config/load.ts";
|
|
8
|
-
export type { Config, ToolProviderRef } from "./config/schema.ts";
|
|
8
|
+
export type { Config, ModelConfig, ToolProviderRef } from "./config/schema.ts";
|
|
9
9
|
export { LANGUAGES, type Language } from "./config/schema.ts";
|
|
10
10
|
export { ERROR_CODES, type ErrorCode, isOpenshainError, OpenshainError } from "./errors.ts";
|
|
11
11
|
export {
|
|
@@ -35,6 +35,7 @@ export {
|
|
|
35
35
|
type ToolSummary,
|
|
36
36
|
} from "./runtime.ts";
|
|
37
37
|
export { jsonSchemas, type SchemaName } from "./schemas.ts";
|
|
38
|
+
export { ASK_USER, RUNTIME_PROVIDER_ID } from "./tool/ask-user.ts";
|
|
38
39
|
export { loadToolModule } from "./tool/load-module.ts";
|
|
39
40
|
export { RESERVED_PATHS, resolveWorkspacePath } from "./tool/paths.ts";
|
|
40
41
|
export {
|
|
@@ -56,6 +57,7 @@ export {
|
|
|
56
57
|
type ToolResult,
|
|
57
58
|
} from "./tool/types.ts";
|
|
58
59
|
export { compileInputValidator, type InputValidation } from "./tool/validate.ts";
|
|
60
|
+
export { uuidv7 } from "./uuid.ts";
|
|
59
61
|
export { verifyArtifact } from "./work/artifacts.ts";
|
|
60
62
|
export { EVENTS_FILE_NAME, EventLog, type NewEvent } from "./work/event-log.ts";
|
|
61
63
|
export {
|
|
@@ -70,7 +72,9 @@ export {
|
|
|
70
72
|
type EventType,
|
|
71
73
|
eventFromFile,
|
|
72
74
|
eventToFile,
|
|
75
|
+
isKnownEventType,
|
|
73
76
|
type ModelUsage,
|
|
77
|
+
parsePayloadFile,
|
|
74
78
|
payloadFileSchemas,
|
|
75
79
|
type StopReason,
|
|
76
80
|
TOOL_REJECTION_CODES,
|
|
@@ -78,6 +82,15 @@ export {
|
|
|
78
82
|
type ToolRejectionCode,
|
|
79
83
|
type UnknownEvent,
|
|
80
84
|
} from "./work/events.ts";
|
|
85
|
+
export {
|
|
86
|
+
countToolCalls,
|
|
87
|
+
type FailureReason,
|
|
88
|
+
type HistoryCall,
|
|
89
|
+
type PendingQuestion,
|
|
90
|
+
pendingQuestions,
|
|
91
|
+
type WorkHistory,
|
|
92
|
+
workHistory,
|
|
93
|
+
} from "./work/history.ts";
|
|
81
94
|
export { acquireLock, LOCK_FILE_NAME, type Lock } from "./work/lock.ts";
|
|
82
95
|
export { buildProjection, type Projection, type ProjectionInput } from "./work/projection.ts";
|
|
83
96
|
export {
|
package/src/runtime.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from "./config/load.ts";
|
|
2
|
-
import type { Config } from "./config/schema.ts";
|
|
2
|
+
import type { Config, ModelConfig } from "./config/schema.ts";
|
|
3
3
|
import { isOpenshainError, OpenshainError } from "./errors.ts";
|
|
4
4
|
import type { ModelProvider } from "./model/types.ts";
|
|
5
5
|
import { loadToolModule } from "./tool/load-module.ts";
|
|
@@ -12,7 +12,7 @@ import { type WorkHandle, WorkStore } from "./work/store.ts";
|
|
|
12
12
|
|
|
13
13
|
export interface RuntimeProviders {
|
|
14
14
|
/** Model providers by the id used in openshain.yaml. */
|
|
15
|
-
models: Record<string, (model:
|
|
15
|
+
models: Record<string, (model: ModelConfig) => ModelProvider>;
|
|
16
16
|
/** Tool providers by the id used in openshain.yaml. Modules are loaded from the config directly. */
|
|
17
17
|
tools: Record<string, () => ToolProvider>;
|
|
18
18
|
}
|
|
@@ -50,6 +50,12 @@ export async function createRuntime(options: CreateRuntimeOptions): Promise<Runt
|
|
|
50
50
|
const { workspaceRoot, providers } = options;
|
|
51
51
|
const config = await loadConfig(workspaceRoot, { modelProviders: Object.keys(providers.models) });
|
|
52
52
|
|
|
53
|
+
if (!config.model) {
|
|
54
|
+
throw new OpenshainError(
|
|
55
|
+
"config",
|
|
56
|
+
"this needs a model: add a model section to openshain.yaml (only the interactive CLI needs one)",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
53
59
|
const modelFactory = Object.hasOwn(providers.models, config.model.provider)
|
|
54
60
|
? providers.models[config.model.provider]
|
|
55
61
|
: undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ASK_USER_TOOL_NAME, type ToolDefinition } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/** The provider id the runtime records for the tools it runs itself. */
|
|
4
|
+
export const RUNTIME_PROVIDER_ID = "runtime";
|
|
5
|
+
|
|
6
|
+
/** The one tool the runtime itself provides: stop and ask the person. */
|
|
7
|
+
export const ASK_USER: Readonly<ToolDefinition> = Object.freeze({
|
|
8
|
+
name: ASK_USER_TOOL_NAME,
|
|
9
|
+
description:
|
|
10
|
+
"Ask the person you work for a question when you cannot proceed without their answer. Use it sparingly; prefer the workspace over guessing. The work waits until the answer is recorded with work_answer.",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
question: {
|
|
15
|
+
type: "string",
|
|
16
|
+
minLength: 1,
|
|
17
|
+
maxLength: 10_000,
|
|
18
|
+
description: "The question, in the person's language.",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: ["question"],
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
},
|
|
24
|
+
effect: "observe",
|
|
25
|
+
});
|
package/src/tool/types.ts
CHANGED
package/src/uuid.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UUID v7 (RFC 9562) without a runtime-specific API, so the same code runs on Node and Bun.
|
|
3
|
+
* Ids made in one process within the same millisecond stay in creation order: the 12 random
|
|
4
|
+
* bits after the timestamp act as a counter until the clock moves on.
|
|
5
|
+
*/
|
|
6
|
+
let lastMs = 0;
|
|
7
|
+
let counter = 0;
|
|
8
|
+
|
|
9
|
+
export function uuidv7(now: number = Date.now()): string {
|
|
10
|
+
const bytes = new Uint8Array(16);
|
|
11
|
+
crypto.getRandomValues(bytes);
|
|
12
|
+
if (now > lastMs) {
|
|
13
|
+
lastMs = now;
|
|
14
|
+
counter = (((bytes[6] as number) & 0x07) << 8) | (bytes[7] as number);
|
|
15
|
+
} else {
|
|
16
|
+
now = lastMs;
|
|
17
|
+
counter = (counter + 1) & 0x0fff;
|
|
18
|
+
if (counter === 0) {
|
|
19
|
+
lastMs = now = lastMs + 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
bytes[0] = Math.floor(now / 2 ** 40) & 0xff;
|
|
23
|
+
bytes[1] = Math.floor(now / 2 ** 32) & 0xff;
|
|
24
|
+
bytes[2] = (now >>> 24) & 0xff;
|
|
25
|
+
bytes[3] = (now >>> 16) & 0xff;
|
|
26
|
+
bytes[4] = (now >>> 8) & 0xff;
|
|
27
|
+
bytes[5] = now & 0xff;
|
|
28
|
+
bytes[6] = 0x70 | (counter >>> 8);
|
|
29
|
+
bytes[7] = counter & 0xff;
|
|
30
|
+
bytes[8] = ((bytes[8] as number) & 0x3f) | 0x80;
|
|
31
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
32
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
33
|
+
}
|
package/src/work/events.ts
CHANGED
|
@@ -41,6 +41,7 @@ export const TOOL_REJECTION_CODES = [
|
|
|
41
41
|
"reserved_path",
|
|
42
42
|
"outside_workspace",
|
|
43
43
|
"invalid_path",
|
|
44
|
+
"limit_reached",
|
|
44
45
|
] as const;
|
|
45
46
|
|
|
46
47
|
export type ToolRejectionCode = (typeof TOOL_REJECTION_CODES)[number];
|
|
@@ -73,6 +74,8 @@ export interface EventPayloads {
|
|
|
73
74
|
"human.input_provided": { callId: string; answer: string };
|
|
74
75
|
/** What the person said in a session. Becomes a user message in the projection. */
|
|
75
76
|
"human.message": { text: string };
|
|
77
|
+
/** A prompt command expanded for the model: its name, where it came from, and the text handed over. */
|
|
78
|
+
"prompt.expanded": { name: string; source: string; text: string };
|
|
76
79
|
"usage.recorded":
|
|
77
80
|
| { kind: "model_inference"; provider: string; model: string; usage: ModelUsage }
|
|
78
81
|
| { kind: "tool_execution"; provider: string; usage: { durationMs: number } };
|
|
@@ -179,6 +182,7 @@ export const payloadFileSchemas = {
|
|
|
179
182
|
"human.input_requested": z.looseObject({ call_id: z.string(), question: z.string() }),
|
|
180
183
|
"human.input_provided": z.looseObject({ call_id: z.string(), answer: z.string() }),
|
|
181
184
|
"human.message": z.looseObject({ text: z.string() }),
|
|
185
|
+
"prompt.expanded": z.looseObject({ name: z.string(), source: z.string(), text: z.string() }),
|
|
182
186
|
"usage.recorded": z.discriminatedUnion("kind", [
|
|
183
187
|
z.looseObject({
|
|
184
188
|
kind: z.literal("model_inference"),
|
|
@@ -309,6 +313,22 @@ export function eventFromFile(input: unknown): AnyEvent {
|
|
|
309
313
|
} as Event;
|
|
310
314
|
}
|
|
311
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Validates a payload given in the file form (snake_case, as in spec/schemas/events.v1.json) and
|
|
318
|
+
* returns it in the in-memory form. For events a client hands the runtime to record.
|
|
319
|
+
*/
|
|
320
|
+
export function parsePayloadFile<T extends EventType>(type: T, payload: unknown): EventPayloads[T] {
|
|
321
|
+
const parsed = payloadFileSchemas[type].safeParse(payload);
|
|
322
|
+
if (!parsed.success) {
|
|
323
|
+
throw new OpenshainError("invalid_event", `${type} payload: ${describeIssues(parsed.error)}`);
|
|
324
|
+
}
|
|
325
|
+
return payloadFromFile(type, parsed.data as FilePayload<T>);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function isKnownEventType(type: string): type is EventType {
|
|
329
|
+
return isKnownType(type);
|
|
330
|
+
}
|
|
331
|
+
|
|
312
332
|
function isKnownType(type: string): type is EventType {
|
|
313
333
|
return Object.hasOwn(payloadFileSchemas, type);
|
|
314
334
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { AnyEvent, Event } from "./events.ts";
|
|
2
|
+
|
|
3
|
+
/** Why a client gives up on a work, as recorded in `work.failed`. */
|
|
4
|
+
export type FailureReason = "limit_reached" | "model_refusal" | "model_error";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Counts the tool calls of a work the way the limits do: every call the runtime started, plus
|
|
8
|
+
* every rejection that never became a call. A rejection of a started call is not a second call.
|
|
9
|
+
*/
|
|
10
|
+
export function countToolCalls(events: readonly AnyEvent[]): number {
|
|
11
|
+
let count = 0;
|
|
12
|
+
let started = new Set<string>();
|
|
13
|
+
for (const event of events) {
|
|
14
|
+
if (event.type === "model.completed") started = new Set();
|
|
15
|
+
else if (event.type === "tool.called") {
|
|
16
|
+
started.add((event as Event<"tool.called">).payload.callId);
|
|
17
|
+
count += 1;
|
|
18
|
+
} else if (event.type === "tool.rejected") {
|
|
19
|
+
if (!started.has((event as Event<"tool.rejected">).payload.callId)) count += 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return count;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PendingQuestion {
|
|
26
|
+
callId: string;
|
|
27
|
+
question: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The questions of the work that have no answer yet, oldest first. Call ids of questions are
|
|
32
|
+
* minted by the runtime, so the whole log is searched: a client recording its own model turns
|
|
33
|
+
* must not hide a question.
|
|
34
|
+
*/
|
|
35
|
+
export function pendingQuestions(events: readonly AnyEvent[]): PendingQuestion[] {
|
|
36
|
+
const answered = new Set(
|
|
37
|
+
events
|
|
38
|
+
.filter((e): e is Event<"human.input_provided"> => e.type === "human.input_provided")
|
|
39
|
+
.map((e) => e.payload.callId),
|
|
40
|
+
);
|
|
41
|
+
return events
|
|
42
|
+
.filter((e): e is Event<"human.input_requested"> => e.type === "human.input_requested")
|
|
43
|
+
.filter((e) => !answered.has(e.payload.callId))
|
|
44
|
+
.map((e) => ({ callId: e.payload.callId, question: e.payload.question }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface HistoryCall {
|
|
48
|
+
callId: string;
|
|
49
|
+
name: string;
|
|
50
|
+
/** The path the call named, when its input had one. */
|
|
51
|
+
path?: string;
|
|
52
|
+
/** Present once the call has a result; absent while it is still open. */
|
|
53
|
+
isError?: boolean;
|
|
54
|
+
rejected?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface WorkHistory {
|
|
58
|
+
calls: HistoryCall[];
|
|
59
|
+
/** Calls that were started but have no result: the work stopped while they ran. */
|
|
60
|
+
unfinished: HistoryCall[];
|
|
61
|
+
pending: PendingQuestion[];
|
|
62
|
+
toolCalls: number;
|
|
63
|
+
/** Model calls recorded on the work, for a client that counts them against a limit. */
|
|
64
|
+
modelCalls: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** What a client needs to pick a work up where it stopped. Built from the log alone. */
|
|
68
|
+
export function workHistory(events: readonly AnyEvent[]): WorkHistory {
|
|
69
|
+
const calls: HistoryCall[] = [];
|
|
70
|
+
const byId = new Map<string, HistoryCall>();
|
|
71
|
+
for (const event of events) {
|
|
72
|
+
if (event.type === "tool.called") {
|
|
73
|
+
const { callId, name, input } = (event as Event<"tool.called">).payload;
|
|
74
|
+
const path = (input as { path?: unknown } | null)?.path;
|
|
75
|
+
const call: HistoryCall = { callId, name, ...(typeof path === "string" && { path }) };
|
|
76
|
+
calls.push(call);
|
|
77
|
+
byId.set(callId, call);
|
|
78
|
+
} else if (event.type === "tool.completed") {
|
|
79
|
+
const { callId, isError } = (event as Event<"tool.completed">).payload;
|
|
80
|
+
const call = byId.get(callId);
|
|
81
|
+
if (call) call.isError = isError;
|
|
82
|
+
} else if (event.type === "tool.rejected") {
|
|
83
|
+
const { callId, name, code } = (event as Event<"tool.rejected">).payload;
|
|
84
|
+
const call = byId.get(callId);
|
|
85
|
+
if (call) call.rejected = code;
|
|
86
|
+
else calls.push({ callId, name, rejected: code });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
calls,
|
|
91
|
+
unfinished: calls.filter((c) => c.isError === undefined && c.rejected === undefined),
|
|
92
|
+
pending: pendingQuestions(events),
|
|
93
|
+
toolCalls: countToolCalls(events),
|
|
94
|
+
modelCalls: events.filter((e) => e.type === "model.requested").length,
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/work/projection.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function buildProjection(input: ProjectionInput): Projection {
|
|
|
40
40
|
...(agentName
|
|
41
41
|
? [`あなたの名前は ${agentName}。名乗るときはこの名前と、社員エージェントであることを言う。`]
|
|
42
42
|
: []),
|
|
43
|
-
"件数、合計、検索の結果は Tool
|
|
43
|
+
"件数、合計、検索の結果は Tool が返した値をそのまま使い、自分で数えたり合計したりしない。各ターンの最後に Runtime が「残り model 呼び出し N 回、Tool 呼び出し M 回」という 1 行を user message として追加する。これは残量の通知で、返事は要らない。依頼が終わったら、何をしたかを要約して終える。",
|
|
44
44
|
].join("\n\n");
|
|
45
45
|
|
|
46
46
|
const messages: ModelMessage[] = [];
|
|
@@ -61,6 +61,9 @@ export function buildProjection(input: ProjectionInput): Projection {
|
|
|
61
61
|
case "human.message":
|
|
62
62
|
pushUserPart({ type: "text", text: (event as Event<"human.message">).payload.text });
|
|
63
63
|
break;
|
|
64
|
+
case "prompt.expanded":
|
|
65
|
+
pushUserPart({ type: "text", text: (event as Event<"prompt.expanded">).payload.text });
|
|
66
|
+
break;
|
|
64
67
|
case "model.completed": {
|
|
65
68
|
const content = (event as Event<"model.completed">).payload.content
|
|
66
69
|
.filter((part) => part.type !== "opaque" || part.provider === input.providerId)
|