@openshain/agent 0.1.1 → 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/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/loop.d.ts +37 -0
- package/dist/loop.js +382 -0
- package/dist/names.d.ts +9 -0
- package/dist/names.js +79 -0
- package/dist/providers/anthropic.d.ts +40 -0
- package/dist/providers/anthropic.js +214 -0
- package/dist/providers/openai-compatible.d.ts +47 -0
- package/dist/providers/openai-compatible.js +251 -0
- package/dist/session.d.ts +41 -0
- package/dist/session.js +394 -0
- package/dist/testing/fake-model.d.ts +29 -0
- package/dist/testing/fake-model.js +39 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/package.json +22 -7
- package/src/session.ts +1 -1
package/dist/session.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { buildProjection, compileInputValidator, isOpenshainError, isTerminal, parseWorkId, SESSION_WORK_TYPE, } from "@openshain/core";
|
|
2
|
+
import { countToolCalls, pendingQuestions, RUNTIME_PROVIDER_ID, runWork } from "./loop.js";
|
|
3
|
+
import { pickAgentName } from "./names.js";
|
|
4
|
+
/** How much one turn of a session may do before the person hears back. */
|
|
5
|
+
export const TURN_LIMITS = { modelCalls: 5, toolCalls: 10 };
|
|
6
|
+
/** What the session's model may do: hand work out and look work up. It never touches files itself. */
|
|
7
|
+
export const SESSION_TOOLS = Object.freeze([
|
|
8
|
+
{
|
|
9
|
+
name: "work_run",
|
|
10
|
+
description: "Start a work for the person's request and drive it until it completes, fails or stops to ask the person a question. Write the objective in the person's own words and add what the conversation established that the work needs to know. Returns the work's id, status, summary, artifacts and usage, or the question it is waiting on.",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
objective: {
|
|
15
|
+
type: "string",
|
|
16
|
+
minLength: 1,
|
|
17
|
+
description: "The request, in the person's words.",
|
|
18
|
+
},
|
|
19
|
+
type: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "A short label for the kind of work. Defaults to request.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
required: ["objective"],
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
},
|
|
27
|
+
effect: "mutate",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "work_list",
|
|
31
|
+
description: "The most recent works in this workspace, newest first, without sessions.",
|
|
32
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
33
|
+
effect: "observe",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "work_show",
|
|
37
|
+
description: "One work by id: its status, summary, artifacts, usage, and who has to act next.",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: { id: { type: "string", minLength: 1 } },
|
|
41
|
+
required: ["id"],
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
},
|
|
44
|
+
effect: "observe",
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
const validators = new Map(SESSION_TOOLS.map((tool) => [tool.name, compileInputValidator(tool.inputSchema)]));
|
|
48
|
+
const ROLE = "あなたはこの会社の社員エージェントとして、受付の役で、この人と話す。作業が要るときは work_run に objective を渡して Work にする。objective は人の言葉で書き、会話で分かった前提を添える。会社のファイルは自分では変更しない。ファイルの中身を見ないと答えられない質問も、work_run で Work にして調べる。作業の結果は要約して伝える。件数や金額は Work の結果の数字をそのまま書き、計算し直さない。返答は端末の画面に出るので、Markdown の記法や絵文字は使わず、短い文で書く。過去の作業は work_list と work_show で答える。";
|
|
49
|
+
/** Opens a conversation, recorded as a work of type "session", between the person and the model. */
|
|
50
|
+
export async function createSession(runtime, options = {}) {
|
|
51
|
+
const model = options.model ?? runtime.model;
|
|
52
|
+
const { principal, profession } = runtime.config;
|
|
53
|
+
const agentName = options.agentName ?? pickAgentName(runtime.config.company.language, await namesInUse(runtime));
|
|
54
|
+
const created = await runtime.works.create({
|
|
55
|
+
objective: "会話",
|
|
56
|
+
principal: principal.id,
|
|
57
|
+
profession: profession.id,
|
|
58
|
+
type: SESSION_WORK_TYPE,
|
|
59
|
+
agentName,
|
|
60
|
+
});
|
|
61
|
+
await withHandle(runtime, created.id, options, (handle) => handle.transition("in_progress", "session opened"));
|
|
62
|
+
return {
|
|
63
|
+
id: created.id,
|
|
64
|
+
agentName,
|
|
65
|
+
turn: (text, turnOptions = {}) => withHandle(runtime, created.id, options, async (handle) => {
|
|
66
|
+
await handle.append({ type: "human.message", payload: { text } });
|
|
67
|
+
return runTurn(runtime, handle, model, options, turnOptions.signal);
|
|
68
|
+
}),
|
|
69
|
+
close: () => withHandle(runtime, created.id, options, async (handle) => {
|
|
70
|
+
if (isTerminal((await handle.current()).status))
|
|
71
|
+
return handle.current();
|
|
72
|
+
await handle.append({
|
|
73
|
+
type: "evidence.recorded",
|
|
74
|
+
payload: { claim: "会話を終了", refs: [], artifacts: [] },
|
|
75
|
+
});
|
|
76
|
+
await handle.append({ type: "work.completed", payload: { summary: "会話を終了" } });
|
|
77
|
+
return handle.current();
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** The names of the sessions still open, so two people talking at once do not get the same one. */
|
|
82
|
+
async function namesInUse(runtime) {
|
|
83
|
+
const { works } = await runtime.works.list();
|
|
84
|
+
return works
|
|
85
|
+
.filter((w) => w.type === SESSION_WORK_TYPE && !isTerminal(w.status))
|
|
86
|
+
.flatMap((w) => (w.agentName ? [w.agentName] : []));
|
|
87
|
+
}
|
|
88
|
+
async function withHandle(runtime, id, options, fn) {
|
|
89
|
+
const opened = await runtime.works.open(id);
|
|
90
|
+
const handle = options.onEvent ? observed(opened, options.onEvent) : opened;
|
|
91
|
+
try {
|
|
92
|
+
return await fn(handle);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await handle.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function runTurn(runtime, handle, model, options, signal) {
|
|
99
|
+
const description = model.describe();
|
|
100
|
+
const config = {
|
|
101
|
+
...runtime.config,
|
|
102
|
+
profession: {
|
|
103
|
+
...runtime.config.profession,
|
|
104
|
+
instructions: `${runtime.config.profession.instructions.trim()}\n\n${ROLE}`,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const turnStart = (await handle.events()).length;
|
|
108
|
+
const tools = [...SESSION_TOOLS];
|
|
109
|
+
let modelCalls = 0;
|
|
110
|
+
let toolCalls = 0;
|
|
111
|
+
for (;;) {
|
|
112
|
+
if (signal?.aborted)
|
|
113
|
+
return { reply: "", stopped: "aborted" };
|
|
114
|
+
if (modelCalls >= TURN_LIMITS.modelCalls) {
|
|
115
|
+
return {
|
|
116
|
+
reply: "",
|
|
117
|
+
stopped: "turn_limit",
|
|
118
|
+
detail: `model calls in one turn (${TURN_LIMITS.modelCalls})`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const events = await handle.events();
|
|
122
|
+
const projection = buildProjection({
|
|
123
|
+
events,
|
|
124
|
+
config,
|
|
125
|
+
tools,
|
|
126
|
+
providerId: model.id,
|
|
127
|
+
budget: {
|
|
128
|
+
modelCallsLeft: TURN_LIMITS.modelCalls - modelCalls,
|
|
129
|
+
toolCallsLeft: TURN_LIMITS.toolCalls - toolCalls,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
await handle.append({
|
|
133
|
+
type: "model.requested",
|
|
134
|
+
payload: {
|
|
135
|
+
provider: model.id,
|
|
136
|
+
model: description.model,
|
|
137
|
+
messageCount: projection.messages.length,
|
|
138
|
+
toolNames: tools.map((t) => t.name),
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
modelCalls += 1;
|
|
142
|
+
let response;
|
|
143
|
+
try {
|
|
144
|
+
response = await model.generate({
|
|
145
|
+
system: projection.system,
|
|
146
|
+
messages: projection.messages,
|
|
147
|
+
tools: projection.tools,
|
|
148
|
+
maxOutputTokens: runtime.config.limits.maxOutputTokens,
|
|
149
|
+
budget: projection.budget,
|
|
150
|
+
stableMessages: projection.messages.length - 1,
|
|
151
|
+
...(runtime.config.model.options && { providerOptions: runtime.config.model.options }),
|
|
152
|
+
}, signal);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
156
|
+
await handle.append({
|
|
157
|
+
type: "model.failed",
|
|
158
|
+
payload: { code: isOpenshainError(err) ? err.code : "model_error", message },
|
|
159
|
+
});
|
|
160
|
+
return { reply: "", stopped: signal?.aborted ? "aborted" : "model_error", detail: message };
|
|
161
|
+
}
|
|
162
|
+
await handle.append({
|
|
163
|
+
type: "model.completed",
|
|
164
|
+
payload: {
|
|
165
|
+
stopReason: response.stopReason,
|
|
166
|
+
content: response.message.content,
|
|
167
|
+
...(runtime.config.debug.persistRaw && response.raw !== undefined && { raw: response.raw }),
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
await handle.append({
|
|
171
|
+
type: "usage.recorded",
|
|
172
|
+
payload: {
|
|
173
|
+
kind: "model_inference",
|
|
174
|
+
provider: model.id,
|
|
175
|
+
model: description.model,
|
|
176
|
+
usage: response.usage,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
const text = textOf(response.message.content);
|
|
180
|
+
switch (response.stopReason) {
|
|
181
|
+
case "end_turn":
|
|
182
|
+
return { reply: text };
|
|
183
|
+
case "tool_call": {
|
|
184
|
+
const calls = response.message.content.filter((p) => p.type === "tool_call");
|
|
185
|
+
for (const call of calls) {
|
|
186
|
+
if (signal?.aborted)
|
|
187
|
+
return { reply: text, stopped: "aborted" };
|
|
188
|
+
if (toolCalls >= TURN_LIMITS.toolCalls) {
|
|
189
|
+
return {
|
|
190
|
+
reply: text,
|
|
191
|
+
stopped: "turn_limit",
|
|
192
|
+
detail: `tool calls in one turn (${TURN_LIMITS.toolCalls})`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
toolCalls += 1;
|
|
196
|
+
await callSessionTool(runtime, handle, model, options, signal, call);
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "max_tokens":
|
|
201
|
+
return { reply: text, stopped: "max_tokens" };
|
|
202
|
+
case "refusal":
|
|
203
|
+
return { reply: text, stopped: "refusal" };
|
|
204
|
+
default:
|
|
205
|
+
return {
|
|
206
|
+
reply: text,
|
|
207
|
+
stopped: "model_error",
|
|
208
|
+
detail: `unexpected stop reason "${response.stopReason}"`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// Nothing of the turn's own events is needed below; the projection rebuilds from the log.
|
|
212
|
+
void turnStart;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function callSessionTool(runtime, handle, model, options, signal, call) {
|
|
216
|
+
const validate = validators.get(call.name);
|
|
217
|
+
if (!validate) {
|
|
218
|
+
await handle.append({
|
|
219
|
+
type: "tool.rejected",
|
|
220
|
+
payload: {
|
|
221
|
+
callId: call.id,
|
|
222
|
+
name: call.name,
|
|
223
|
+
code: "unknown_tool",
|
|
224
|
+
reason: `no tool named "${call.name}"; the session offers ${SESSION_TOOLS.map((t) => t.name).join(", ")}`,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const validation = validate(call.input);
|
|
230
|
+
if (!validation.ok) {
|
|
231
|
+
await handle.append({
|
|
232
|
+
type: "tool.rejected",
|
|
233
|
+
payload: {
|
|
234
|
+
callId: call.id,
|
|
235
|
+
name: call.name,
|
|
236
|
+
code: "schema_mismatch",
|
|
237
|
+
reason: `input does not match the schema of ${call.name}: ${validation.reason}`,
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
await handle.append({
|
|
243
|
+
type: "tool.called",
|
|
244
|
+
payload: { callId: call.id, provider: RUNTIME_PROVIDER_ID, name: call.name, input: call.input },
|
|
245
|
+
});
|
|
246
|
+
let result;
|
|
247
|
+
try {
|
|
248
|
+
result = await runSessionTool(runtime, handle, model, options, signal, call.name, call.input);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
result = {
|
|
252
|
+
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
|
|
253
|
+
isError: true,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
await handle.append({
|
|
257
|
+
type: "tool.completed",
|
|
258
|
+
payload: { callId: call.id, content: result.content, isError: result.isError ?? false },
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
async function runSessionTool(runtime, handle, model, options, signal, name, input) {
|
|
262
|
+
switch (name) {
|
|
263
|
+
case "work_run": {
|
|
264
|
+
const type = typeof input.type === "string" && input.type !== "" ? input.type : "request";
|
|
265
|
+
if (type === SESSION_WORK_TYPE) {
|
|
266
|
+
return {
|
|
267
|
+
content: [
|
|
268
|
+
{
|
|
269
|
+
type: "text",
|
|
270
|
+
text: `type "${SESSION_WORK_TYPE}" is reserved for conversations; use another label, such as request`,
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
isError: true,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const agentName = (await handle.current()).agentName;
|
|
277
|
+
const child = await runtime.works.create({
|
|
278
|
+
objective: String(input.objective),
|
|
279
|
+
principal: runtime.config.principal.id,
|
|
280
|
+
profession: runtime.config.profession.id,
|
|
281
|
+
type,
|
|
282
|
+
parent: handle.id,
|
|
283
|
+
...(agentName !== undefined && { agentName }),
|
|
284
|
+
});
|
|
285
|
+
const done = await runWork(runtime, child.id, {
|
|
286
|
+
model,
|
|
287
|
+
...(options.onInput && {
|
|
288
|
+
onInput: (q) => options.onInput?.(child.id, q),
|
|
289
|
+
}),
|
|
290
|
+
...(options.onWorkEvent && {
|
|
291
|
+
onEvent: (e) => options.onWorkEvent?.(child.id, e),
|
|
292
|
+
}),
|
|
293
|
+
...(signal && { signal }),
|
|
294
|
+
});
|
|
295
|
+
return {
|
|
296
|
+
content: [
|
|
297
|
+
{ type: "json", value: await describeWork(runtime, done, signal?.aborted === true) },
|
|
298
|
+
],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
case "work_list": {
|
|
302
|
+
const { works } = await runtime.works.list();
|
|
303
|
+
const recent = works
|
|
304
|
+
.filter((w) => w.type !== SESSION_WORK_TYPE)
|
|
305
|
+
.sort((a, b) => (a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0))
|
|
306
|
+
.slice(0, 20)
|
|
307
|
+
.map((w) => ({
|
|
308
|
+
id: w.id,
|
|
309
|
+
status: w.status,
|
|
310
|
+
type: w.type,
|
|
311
|
+
objective: w.objective,
|
|
312
|
+
createdAt: w.createdAt,
|
|
313
|
+
}));
|
|
314
|
+
return { content: [{ type: "json", value: { works: recent } }] };
|
|
315
|
+
}
|
|
316
|
+
case "work_show": {
|
|
317
|
+
let id;
|
|
318
|
+
try {
|
|
319
|
+
id = parseWorkId(String(input.id));
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return {
|
|
323
|
+
content: [{ type: "text", text: `"${String(input.id)}" is not a work id` }],
|
|
324
|
+
isError: true,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const work = await runtime.works.get(id);
|
|
328
|
+
return { content: [{ type: "json", value: await describeWork(runtime, work, false) }] };
|
|
329
|
+
}
|
|
330
|
+
default:
|
|
331
|
+
return { content: [{ type: "text", text: `no tool named "${name}"` }], isError: true };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/** A work as the session's model needs to see it: outcome, cost, and who acts next. */
|
|
335
|
+
async function describeWork(runtime, work, interrupted) {
|
|
336
|
+
const events = await runtime.works.events(work.id);
|
|
337
|
+
const usage = {
|
|
338
|
+
modelCalls: 0,
|
|
339
|
+
toolCalls: countToolCalls(events),
|
|
340
|
+
inputTokens: 0,
|
|
341
|
+
outputTokens: 0,
|
|
342
|
+
};
|
|
343
|
+
for (const event of events) {
|
|
344
|
+
if (event.type === "model.requested")
|
|
345
|
+
usage.modelCalls += 1;
|
|
346
|
+
if (event.type === "usage.recorded") {
|
|
347
|
+
const { payload } = event;
|
|
348
|
+
if (payload.kind === "model_inference") {
|
|
349
|
+
usage.inputTokens += payload.usage.inputTokens;
|
|
350
|
+
usage.outputTokens += payload.usage.outputTokens;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const question = work.status === "waiting_input" ? pendingQuestions(events).map((q) => q.question) : [];
|
|
355
|
+
return {
|
|
356
|
+
id: work.id,
|
|
357
|
+
status: work.status,
|
|
358
|
+
...(work.outcome && { summary: work.outcome.summary, artifacts: work.outcome.artifacts }),
|
|
359
|
+
...(work.failure && { failure: work.failure }),
|
|
360
|
+
...(question.length > 0 && { waitingFor: question }),
|
|
361
|
+
...(interrupted &&
|
|
362
|
+
work.status === "in_progress" && {
|
|
363
|
+
interrupted: "the person stopped this work; it can be resumed",
|
|
364
|
+
}),
|
|
365
|
+
nextActor: isTerminal(work.status)
|
|
366
|
+
? "nobody"
|
|
367
|
+
: work.status === "waiting_input"
|
|
368
|
+
? "person"
|
|
369
|
+
: "model",
|
|
370
|
+
usage,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function textOf(content) {
|
|
374
|
+
return content
|
|
375
|
+
.filter((p) => p.type === "text")
|
|
376
|
+
.map((p) => p.text)
|
|
377
|
+
.join("\n")
|
|
378
|
+
.trim();
|
|
379
|
+
}
|
|
380
|
+
function observed(handle, onEvent) {
|
|
381
|
+
return {
|
|
382
|
+
...handle,
|
|
383
|
+
async append(event) {
|
|
384
|
+
const recorded = await handle.append(event);
|
|
385
|
+
await onEvent(recorded);
|
|
386
|
+
return recorded;
|
|
387
|
+
},
|
|
388
|
+
async transition(to, reason) {
|
|
389
|
+
const recorded = await handle.transition(to, reason);
|
|
390
|
+
await onEvent(recorded);
|
|
391
|
+
return recorded;
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ModelProvider, ModelRequest, ModelResponse } from "@openshain/core";
|
|
2
|
+
/** One scripted answer: a response, or a function that builds one from the request. */
|
|
3
|
+
export type FakeStep = ModelResponse | ((request: ModelRequest) => ModelResponse);
|
|
4
|
+
/**
|
|
5
|
+
* A model that answers from a script, one response per call, and remembers
|
|
6
|
+
* every request it saw. For tests and for trying tools without a real model.
|
|
7
|
+
*/
|
|
8
|
+
export declare class FakeModelProvider implements ModelProvider {
|
|
9
|
+
readonly id = "fake";
|
|
10
|
+
readonly requests: ModelRequest[];
|
|
11
|
+
private readonly steps;
|
|
12
|
+
constructor(steps: FakeStep[]);
|
|
13
|
+
describe(): {
|
|
14
|
+
provider: string;
|
|
15
|
+
model: string;
|
|
16
|
+
capabilities: {
|
|
17
|
+
tools: boolean;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
generate(request: ModelRequest): Promise<ModelResponse>;
|
|
21
|
+
}
|
|
22
|
+
/** A response that ends the turn with text. */
|
|
23
|
+
export declare function say(text: string): ModelResponse;
|
|
24
|
+
/** A response that asks for one or more tool calls. */
|
|
25
|
+
export declare function callTools(...calls: {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
input: unknown;
|
|
29
|
+
}[]): ModelResponse;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A model that answers from a script, one response per call, and remembers
|
|
3
|
+
* every request it saw. For tests and for trying tools without a real model.
|
|
4
|
+
*/
|
|
5
|
+
export class FakeModelProvider {
|
|
6
|
+
id = "fake";
|
|
7
|
+
requests = [];
|
|
8
|
+
steps;
|
|
9
|
+
constructor(steps) {
|
|
10
|
+
this.steps = [...steps];
|
|
11
|
+
}
|
|
12
|
+
describe() {
|
|
13
|
+
return { provider: "fake", model: "fake-1", capabilities: { tools: true } };
|
|
14
|
+
}
|
|
15
|
+
async generate(request) {
|
|
16
|
+
this.requests.push(request);
|
|
17
|
+
const step = this.steps.shift();
|
|
18
|
+
if (!step)
|
|
19
|
+
throw new Error("the fake model ran out of scripted responses");
|
|
20
|
+
return typeof step === "function" ? step(request) : step;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** A response that ends the turn with text. */
|
|
24
|
+
export function say(text) {
|
|
25
|
+
return {
|
|
26
|
+
message: { role: "assistant", content: [{ type: "text", text }] },
|
|
27
|
+
stopReason: "end_turn",
|
|
28
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** A response that asks for one or more tool calls. */
|
|
32
|
+
export function callTools(...calls) {
|
|
33
|
+
const content = calls.map((c) => ({ type: "tool_call", ...c }));
|
|
34
|
+
return {
|
|
35
|
+
message: { role: "assistant", content },
|
|
36
|
+
stopReason: "tool_call",
|
|
37
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { callTools, FakeModelProvider, type FakeStep, say } from "./fake-model.ts";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { callTools, FakeModelProvider, say } from "./fake-model.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openshain/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Tool loop and model providers (bring your own key)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openshain",
|
|
@@ -21,27 +21,42 @@
|
|
|
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
|
-
".":
|
|
34
|
-
|
|
36
|
+
".": {
|
|
37
|
+
"bun": "./src/index.ts",
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./testing": {
|
|
42
|
+
"bun": "./src/testing/index.ts",
|
|
43
|
+
"types": "./dist/testing/index.d.ts",
|
|
44
|
+
"import": "./dist/testing/index.js"
|
|
45
|
+
}
|
|
35
46
|
},
|
|
36
|
-
"
|
|
37
|
-
"
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "../../node_modules/.bin/tsc -p tsconfig.build.json",
|
|
49
|
+
"prepublishOnly": "rm -rf dist && ../../node_modules/.bin/tsc -p tsconfig.build.json"
|
|
38
50
|
},
|
|
39
51
|
"dependencies": {
|
|
40
52
|
"@anthropic-ai/sdk": "0.123.0",
|
|
41
|
-
"@openshain/core": "0.
|
|
53
|
+
"@openshain/core": "0.2.0",
|
|
42
54
|
"openai": "7.10.0"
|
|
43
55
|
},
|
|
44
56
|
"devDependencies": {
|
|
45
|
-
"@openshain/tools": "0.
|
|
57
|
+
"@openshain/tools": "0.2.0"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
46
61
|
}
|
|
47
62
|
}
|
package/src/session.ts
CHANGED
|
@@ -72,7 +72,7 @@ const validators = new Map(
|
|
|
72
72
|
);
|
|
73
73
|
|
|
74
74
|
const ROLE =
|
|
75
|
-
"あなたはこの会社の社員エージェントとして、受付の役で、この人と話す。作業が要るときは work_run に objective を渡して Work にする。objective
|
|
75
|
+
"あなたはこの会社の社員エージェントとして、受付の役で、この人と話す。作業が要るときは work_run に objective を渡して Work にする。objective は人の言葉で書き、会話で分かった前提を添える。会社のファイルは自分では変更しない。ファイルの中身を見ないと答えられない質問も、work_run で Work にして調べる。作業の結果は要約して伝える。件数や金額は Work の結果の数字をそのまま書き、計算し直さない。返答は端末の画面に出るので、Markdown の記法や絵文字は使わず、短い文で書く。過去の作業は work_list と work_show で答える。";
|
|
76
76
|
|
|
77
77
|
export interface SessionOptions {
|
|
78
78
|
/** Defaults to the runtime's model, for the session and for the works it starts. */
|