@openshain/core 0.1.0 → 0.2.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/dist/config/load.d.ts +10 -0
- package/dist/config/load.js +66 -0
- package/dist/config/schema.d.ts +84 -0
- package/dist/config/schema.js +96 -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 +21 -0
- package/dist/index.js +20 -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 +144 -0
- package/dist/schemas.d.ts +9 -0
- package/dist/schemas.js +44 -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 +15 -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 +311 -0
- package/dist/work/events.js +344 -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 +130 -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 +16 -5
- package/src/ids.ts +3 -2
- package/src/index.ts +1 -0
- package/src/uuid.ts +33 -0
- package/src/work/projection.ts +1 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { readdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isOpenshainError, OpenshainError } from "../errors.js";
|
|
4
|
+
import { newWorkId, parseWorkId } from "../ids.js";
|
|
5
|
+
import { EventLog } from "./event-log.js";
|
|
6
|
+
import { acquireLock } from "./lock.js";
|
|
7
|
+
import { reduceWork, transition, workToFile } from "./work.js";
|
|
8
|
+
export const WORK_DIR_NAME = "work";
|
|
9
|
+
export const WORK_FILE_NAME = "work.json";
|
|
10
|
+
/** Works of one workspace, stored under work/<id>/. Reads need no lock; writes go through a handle. */
|
|
11
|
+
export class WorkStore {
|
|
12
|
+
root;
|
|
13
|
+
constructor(root) {
|
|
14
|
+
this.root = root;
|
|
15
|
+
}
|
|
16
|
+
async create(input) {
|
|
17
|
+
const id = newWorkId();
|
|
18
|
+
const dir = this.dir(id);
|
|
19
|
+
const lock = await acquireLock(dir);
|
|
20
|
+
try {
|
|
21
|
+
const log = await EventLog.open(dir, id);
|
|
22
|
+
await log.append({
|
|
23
|
+
type: "work.created",
|
|
24
|
+
payload: {
|
|
25
|
+
objective: input.objective,
|
|
26
|
+
principal: input.principal,
|
|
27
|
+
profession: input.profession,
|
|
28
|
+
type: input.type ?? "request",
|
|
29
|
+
...(input.parent !== undefined && { parent: input.parent }),
|
|
30
|
+
...(input.agentName !== undefined && { agentName: input.agentName }),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
return await this.snapshot(id, await log.read());
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await lock.release();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async get(id) {
|
|
40
|
+
return reduceWork(await this.events(id));
|
|
41
|
+
}
|
|
42
|
+
async list() {
|
|
43
|
+
let names;
|
|
44
|
+
try {
|
|
45
|
+
names = await readdir(join(this.root, WORK_DIR_NAME));
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (err.code === "ENOENT")
|
|
49
|
+
return { works: [], problems: [] };
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
const result = { works: [], problems: [] };
|
|
53
|
+
for (const name of names.sort()) {
|
|
54
|
+
let id;
|
|
55
|
+
try {
|
|
56
|
+
id = parseWorkId(name);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
continue; // not a work directory
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
result.works.push(await this.get(id));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (!isOpenshainError(error))
|
|
66
|
+
throw error;
|
|
67
|
+
result.problems.push({ id, error });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
async events(id) {
|
|
73
|
+
const dir = await this.existingDir(id);
|
|
74
|
+
const log = await EventLog.open(dir, id);
|
|
75
|
+
return log.read();
|
|
76
|
+
}
|
|
77
|
+
/** Takes the work's lock. Call close() when done, or use append()/transition() for a single write. */
|
|
78
|
+
async open(id) {
|
|
79
|
+
const dir = await this.existingDir(id);
|
|
80
|
+
const lock = await acquireLock(dir);
|
|
81
|
+
let log;
|
|
82
|
+
try {
|
|
83
|
+
log = await EventLog.open(dir, id);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
await lock.release();
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
return this.handle(id, log, lock);
|
|
90
|
+
}
|
|
91
|
+
/** Opens, appends one event, refreshes work.json and closes. */
|
|
92
|
+
async append(id, event) {
|
|
93
|
+
const handle = await this.open(id);
|
|
94
|
+
try {
|
|
95
|
+
return await handle.append(event);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await handle.close();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Opens, records one status change and closes. */
|
|
102
|
+
async transition(id, to, reason) {
|
|
103
|
+
const handle = await this.open(id);
|
|
104
|
+
try {
|
|
105
|
+
return await handle.transition(to, reason);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
await handle.close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
handle(id, log, lock) {
|
|
112
|
+
const store = this;
|
|
113
|
+
let open = true;
|
|
114
|
+
const assertOpen = () => {
|
|
115
|
+
if (!open)
|
|
116
|
+
throw new OpenshainError("lock_held", `work ${id} handle is closed`);
|
|
117
|
+
};
|
|
118
|
+
const handle = {
|
|
119
|
+
id,
|
|
120
|
+
async current() {
|
|
121
|
+
return reduceWork(await log.read());
|
|
122
|
+
},
|
|
123
|
+
events() {
|
|
124
|
+
return log.read();
|
|
125
|
+
},
|
|
126
|
+
async append(event) {
|
|
127
|
+
assertOpen();
|
|
128
|
+
const appended = await log.append(event);
|
|
129
|
+
await store.snapshot(id, await log.read());
|
|
130
|
+
return appended;
|
|
131
|
+
},
|
|
132
|
+
async transition(to, reason) {
|
|
133
|
+
assertOpen();
|
|
134
|
+
if (to === "completed" || to === "failed") {
|
|
135
|
+
throw new OpenshainError("invalid_transition", `use work.${to} to move a work to ${to}; status changes cannot end a work`);
|
|
136
|
+
}
|
|
137
|
+
const work = reduceWork(await log.read());
|
|
138
|
+
transition(work.status, to);
|
|
139
|
+
return handle.append({
|
|
140
|
+
type: "work.status_changed",
|
|
141
|
+
payload: { from: work.status, to, reason },
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
async close() {
|
|
145
|
+
if (!open)
|
|
146
|
+
return;
|
|
147
|
+
open = false;
|
|
148
|
+
await lock.release();
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
return handle;
|
|
152
|
+
}
|
|
153
|
+
dir(id) {
|
|
154
|
+
return join(this.root, WORK_DIR_NAME, parseWorkId(id));
|
|
155
|
+
}
|
|
156
|
+
async existingDir(id) {
|
|
157
|
+
const dir = this.dir(id);
|
|
158
|
+
try {
|
|
159
|
+
await stat(dir);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
if (err.code === "ENOENT") {
|
|
163
|
+
throw new OpenshainError("not_found", `work ${id} does not exist in ${this.root}`);
|
|
164
|
+
}
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
return dir;
|
|
168
|
+
}
|
|
169
|
+
async snapshot(id, events) {
|
|
170
|
+
const work = reduceWork(events);
|
|
171
|
+
await writeFile(join(this.dir(id), WORK_FILE_NAME), `${JSON.stringify(workToFile(work), null, 2)}\n`, "utf8");
|
|
172
|
+
return work;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { WorkId } from "../ids.ts";
|
|
3
|
+
import type { AnyEvent, Artifact } from "./events.ts";
|
|
4
|
+
export declare const WORK_STATUSES: readonly ["queued", "in_progress", "waiting_input", "waiting_approval", "waiting_external", "completed", "failed", "cancelled"];
|
|
5
|
+
export declare const WorkStatus: z.ZodEnum<{
|
|
6
|
+
cancelled: "cancelled";
|
|
7
|
+
completed: "completed";
|
|
8
|
+
failed: "failed";
|
|
9
|
+
in_progress: "in_progress";
|
|
10
|
+
queued: "queued";
|
|
11
|
+
waiting_approval: "waiting_approval";
|
|
12
|
+
waiting_external: "waiting_external";
|
|
13
|
+
waiting_input: "waiting_input";
|
|
14
|
+
}>;
|
|
15
|
+
export type WorkStatus = z.infer<typeof WorkStatus>;
|
|
16
|
+
/**
|
|
17
|
+
* The type of the work that records a conversation. Only a session opens one: its objective is a
|
|
18
|
+
* label that stays out of the projection, and it is not run like other works.
|
|
19
|
+
*/
|
|
20
|
+
export declare const SESSION_WORK_TYPE = "session";
|
|
21
|
+
/** Throws invalid_transition unless a work may move from one status to the other. */
|
|
22
|
+
export declare function transition(from: WorkStatus, to: WorkStatus): void;
|
|
23
|
+
export interface Work {
|
|
24
|
+
id: WorkId;
|
|
25
|
+
principal: string;
|
|
26
|
+
profession: string;
|
|
27
|
+
type: string;
|
|
28
|
+
objective: string;
|
|
29
|
+
/** The work this one was started from, if any. */
|
|
30
|
+
parent?: string;
|
|
31
|
+
/** The name the model goes by in this work, when a session gave it one. */
|
|
32
|
+
agentName?: string;
|
|
33
|
+
status: WorkStatus;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
startedAt?: string;
|
|
36
|
+
completedAt?: string;
|
|
37
|
+
outcome?: {
|
|
38
|
+
summary: string;
|
|
39
|
+
artifacts: Artifact[];
|
|
40
|
+
};
|
|
41
|
+
failure?: {
|
|
42
|
+
reason: string;
|
|
43
|
+
detail: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Rebuilds the current state of a work from its event log. The log is the truth. */
|
|
47
|
+
export declare function reduceWork(events: readonly AnyEvent[]): Work;
|
|
48
|
+
export declare function isTerminal(status: WorkStatus): boolean;
|
|
49
|
+
/** Shape of work.json. A projection of the event log, never the source of truth. */
|
|
50
|
+
export declare const WorkFileSchema: z.ZodObject<{
|
|
51
|
+
id: z.ZodString;
|
|
52
|
+
principal: z.ZodString;
|
|
53
|
+
profession: z.ZodString;
|
|
54
|
+
type: z.ZodString;
|
|
55
|
+
objective: z.ZodString;
|
|
56
|
+
parent: z.ZodOptional<z.ZodString>;
|
|
57
|
+
agent_name: z.ZodOptional<z.ZodString>;
|
|
58
|
+
status: z.ZodEnum<{
|
|
59
|
+
cancelled: "cancelled";
|
|
60
|
+
completed: "completed";
|
|
61
|
+
failed: "failed";
|
|
62
|
+
in_progress: "in_progress";
|
|
63
|
+
queued: "queued";
|
|
64
|
+
waiting_approval: "waiting_approval";
|
|
65
|
+
waiting_external: "waiting_external";
|
|
66
|
+
waiting_input: "waiting_input";
|
|
67
|
+
}>;
|
|
68
|
+
created_at: z.ZodISODateTime;
|
|
69
|
+
started_at: z.ZodOptional<z.ZodISODateTime>;
|
|
70
|
+
completed_at: z.ZodOptional<z.ZodISODateTime>;
|
|
71
|
+
outcome: z.ZodOptional<z.ZodObject<{
|
|
72
|
+
summary: z.ZodString;
|
|
73
|
+
artifacts: z.ZodArray<z.ZodObject<{
|
|
74
|
+
path: z.ZodString;
|
|
75
|
+
sha256: z.ZodString;
|
|
76
|
+
missing: z.ZodOptional<z.ZodLiteral<true>>;
|
|
77
|
+
claimed: z.ZodOptional<z.ZodLiteral<true>>;
|
|
78
|
+
}, z.core.$strict>>;
|
|
79
|
+
}, z.core.$strict>>;
|
|
80
|
+
failure: z.ZodOptional<z.ZodObject<{
|
|
81
|
+
reason: z.ZodString;
|
|
82
|
+
detail: z.ZodString;
|
|
83
|
+
}, z.core.$strict>>;
|
|
84
|
+
}, z.core.$strict>;
|
|
85
|
+
export type WorkFile = z.infer<typeof WorkFileSchema>;
|
|
86
|
+
export declare function workToFile(work: Work): WorkFile;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { OpenshainError } from "../errors.js";
|
|
3
|
+
export const WORK_STATUSES = [
|
|
4
|
+
"queued",
|
|
5
|
+
"in_progress",
|
|
6
|
+
"waiting_input",
|
|
7
|
+
"waiting_approval",
|
|
8
|
+
"waiting_external",
|
|
9
|
+
"completed",
|
|
10
|
+
"failed",
|
|
11
|
+
"cancelled",
|
|
12
|
+
];
|
|
13
|
+
export const WorkStatus = z.enum(WORK_STATUSES);
|
|
14
|
+
/**
|
|
15
|
+
* The type of the work that records a conversation. Only a session opens one: its objective is a
|
|
16
|
+
* label that stays out of the projection, and it is not run like other works.
|
|
17
|
+
*/
|
|
18
|
+
export const SESSION_WORK_TYPE = "session";
|
|
19
|
+
const allowed = {
|
|
20
|
+
queued: ["in_progress", "cancelled"],
|
|
21
|
+
in_progress: [
|
|
22
|
+
"waiting_input",
|
|
23
|
+
"waiting_approval",
|
|
24
|
+
"waiting_external",
|
|
25
|
+
"completed",
|
|
26
|
+
"failed",
|
|
27
|
+
"cancelled",
|
|
28
|
+
],
|
|
29
|
+
waiting_input: ["in_progress", "cancelled", "failed"],
|
|
30
|
+
waiting_approval: ["in_progress", "cancelled", "failed"],
|
|
31
|
+
waiting_external: ["in_progress", "cancelled", "failed"],
|
|
32
|
+
completed: [],
|
|
33
|
+
failed: [],
|
|
34
|
+
cancelled: [],
|
|
35
|
+
};
|
|
36
|
+
/** Throws invalid_transition unless a work may move from one status to the other. */
|
|
37
|
+
export function transition(from, to) {
|
|
38
|
+
if (!allowed[from].includes(to)) {
|
|
39
|
+
throw new OpenshainError("invalid_transition", `cannot move work from ${from} to ${to}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Rebuilds the current state of a work from its event log. The log is the truth. */
|
|
43
|
+
export function reduceWork(events) {
|
|
44
|
+
const first = events[0];
|
|
45
|
+
if (first?.type !== "work.created") {
|
|
46
|
+
throw new OpenshainError("corrupt_log", "the first event of a work must be work.created");
|
|
47
|
+
}
|
|
48
|
+
const created = first;
|
|
49
|
+
const work = {
|
|
50
|
+
id: created.workId,
|
|
51
|
+
principal: created.payload.principal,
|
|
52
|
+
profession: created.payload.profession,
|
|
53
|
+
type: created.payload.type,
|
|
54
|
+
objective: created.payload.objective,
|
|
55
|
+
...(created.payload.parent !== undefined && { parent: created.payload.parent }),
|
|
56
|
+
...(created.payload.agentName !== undefined && { agentName: created.payload.agentName }),
|
|
57
|
+
status: "queued",
|
|
58
|
+
createdAt: created.occurredAt,
|
|
59
|
+
};
|
|
60
|
+
let artifacts = [];
|
|
61
|
+
for (const event of events.slice(1)) {
|
|
62
|
+
switch (event.type) {
|
|
63
|
+
case "work.status_changed": {
|
|
64
|
+
const { payload } = event;
|
|
65
|
+
const to = WorkStatus.safeParse(payload.to);
|
|
66
|
+
if (!to.success) {
|
|
67
|
+
throw new OpenshainError("corrupt_log", `unknown work status "${payload.to}"`);
|
|
68
|
+
}
|
|
69
|
+
if (to.data === "completed" || to.data === "failed") {
|
|
70
|
+
throw new OpenshainError("corrupt_log", `work.status_changed may not move a work to ${to.data}; use work.${to.data}`);
|
|
71
|
+
}
|
|
72
|
+
if (payload.from !== work.status) {
|
|
73
|
+
throw new OpenshainError("corrupt_log", `work.status_changed says the work was ${payload.from} but it was ${work.status}`);
|
|
74
|
+
}
|
|
75
|
+
move(work, to.data, event.occurredAt);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case "evidence.recorded":
|
|
79
|
+
if (isTerminal(work.status)) {
|
|
80
|
+
throw new OpenshainError("corrupt_log", `evidence recorded after the work ${work.status}`);
|
|
81
|
+
}
|
|
82
|
+
artifacts = event.payload.artifacts;
|
|
83
|
+
break;
|
|
84
|
+
case "work.completed":
|
|
85
|
+
move(work, "completed", event.occurredAt);
|
|
86
|
+
work.outcome = { summary: event.payload.summary, artifacts };
|
|
87
|
+
break;
|
|
88
|
+
case "work.failed": {
|
|
89
|
+
const { payload } = event;
|
|
90
|
+
move(work, "failed", event.occurredAt);
|
|
91
|
+
work.failure = { reason: payload.reason, detail: payload.detail };
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
default:
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return work;
|
|
99
|
+
}
|
|
100
|
+
export function isTerminal(status) {
|
|
101
|
+
return status === "completed" || status === "failed" || status === "cancelled";
|
|
102
|
+
}
|
|
103
|
+
function move(work, to, at) {
|
|
104
|
+
transition(work.status, to);
|
|
105
|
+
work.status = to;
|
|
106
|
+
if (to === "in_progress" && work.startedAt === undefined)
|
|
107
|
+
work.startedAt = at;
|
|
108
|
+
if (isTerminal(to))
|
|
109
|
+
work.completedAt = at;
|
|
110
|
+
}
|
|
111
|
+
const artifactFile = z.strictObject({
|
|
112
|
+
path: z.string(),
|
|
113
|
+
sha256: z.string(),
|
|
114
|
+
missing: z.literal(true).optional(),
|
|
115
|
+
claimed: z.literal(true).optional(),
|
|
116
|
+
});
|
|
117
|
+
/** Shape of work.json. A projection of the event log, never the source of truth. */
|
|
118
|
+
export const WorkFileSchema = z.strictObject({
|
|
119
|
+
id: z.string(),
|
|
120
|
+
principal: z.string(),
|
|
121
|
+
profession: z.string(),
|
|
122
|
+
type: z.string(),
|
|
123
|
+
objective: z.string(),
|
|
124
|
+
parent: z.string().optional(),
|
|
125
|
+
agent_name: z.string().optional(),
|
|
126
|
+
status: WorkStatus,
|
|
127
|
+
created_at: z.iso.datetime(),
|
|
128
|
+
started_at: z.iso.datetime().optional(),
|
|
129
|
+
completed_at: z.iso.datetime().optional(),
|
|
130
|
+
outcome: z.strictObject({ summary: z.string(), artifacts: z.array(artifactFile) }).optional(),
|
|
131
|
+
failure: z.strictObject({ reason: z.string(), detail: z.string() }).optional(),
|
|
132
|
+
});
|
|
133
|
+
export function workToFile(work) {
|
|
134
|
+
return {
|
|
135
|
+
id: work.id,
|
|
136
|
+
principal: work.principal,
|
|
137
|
+
profession: work.profession,
|
|
138
|
+
type: work.type,
|
|
139
|
+
objective: work.objective,
|
|
140
|
+
...(work.parent !== undefined && { parent: work.parent }),
|
|
141
|
+
...(work.agentName !== undefined && { agent_name: work.agentName }),
|
|
142
|
+
status: work.status,
|
|
143
|
+
created_at: work.createdAt,
|
|
144
|
+
...(work.startedAt !== undefined && { started_at: work.startedAt }),
|
|
145
|
+
...(work.completedAt !== undefined && { completed_at: work.completedAt }),
|
|
146
|
+
...(work.outcome && { outcome: work.outcome }),
|
|
147
|
+
...(work.failure && { failure: work.failure }),
|
|
148
|
+
};
|
|
149
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openshain/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Contracts (provider interfaces), fundamental objects, and the work runtime",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openshain",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
],
|
|
13
13
|
"author": "Hiroki Nakatani",
|
|
14
14
|
"license": "Apache-2.0",
|
|
15
|
-
"homepage": "https://
|
|
15
|
+
"homepage": "https://openshain.jp",
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
18
18
|
"url": "git+https://github.com/openshain/openshain.git",
|
|
@@ -21,24 +21,35 @@
|
|
|
21
21
|
"bugs": "https://github.com/openshain/openshain/issues",
|
|
22
22
|
"type": "module",
|
|
23
23
|
"engines": {
|
|
24
|
+
"node": ">=22",
|
|
24
25
|
"bun": ">=1.3"
|
|
25
26
|
},
|
|
26
27
|
"files": [
|
|
28
|
+
"dist",
|
|
27
29
|
"src",
|
|
28
30
|
"!src/**/*.test.ts",
|
|
31
|
+
"!src/**/*.test.tsx",
|
|
29
32
|
"README.md",
|
|
30
33
|
"LICENSE"
|
|
31
34
|
],
|
|
32
35
|
"exports": {
|
|
33
|
-
".":
|
|
36
|
+
".": {
|
|
37
|
+
"bun": "./src/index.ts",
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js"
|
|
40
|
+
}
|
|
34
41
|
},
|
|
35
|
-
"
|
|
36
|
-
"
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "../../node_modules/.bin/tsc -p tsconfig.build.json",
|
|
44
|
+
"prepublishOnly": "rm -rf dist && ../../node_modules/.bin/tsc -p tsconfig.build.json"
|
|
37
45
|
},
|
|
38
46
|
"dependencies": {
|
|
39
47
|
"ajv": "8.20.0",
|
|
40
48
|
"safe-regex2": "5.1.1",
|
|
41
49
|
"yaml": "2.9.0",
|
|
42
50
|
"zod": "4.5.4"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
43
54
|
}
|
|
44
55
|
}
|
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
|
@@ -56,6 +56,7 @@ export {
|
|
|
56
56
|
type ToolResult,
|
|
57
57
|
} from "./tool/types.ts";
|
|
58
58
|
export { compileInputValidator, type InputValidation } from "./tool/validate.ts";
|
|
59
|
+
export { uuidv7 } from "./uuid.ts";
|
|
59
60
|
export { verifyArtifact } from "./work/artifacts.ts";
|
|
60
61
|
export { EVENTS_FILE_NAME, EventLog, type NewEvent } from "./work/event-log.ts";
|
|
61
62
|
export {
|
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/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[] = [];
|