Repository navigation
Expand file tree
/
Copy pathhost.ts
More file actions
104 lines (95 loc) · 3.7 KB
/
Copy pathhost.ts
File metadata and controls
104 lines (95 loc) · 3.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// The narrow slice of the OpenCode v2 Promise plugin context this plugin depends on.
// Keeping it explicit lets tests inject a fake host and keeps the plugin free of
// runtime imports from `@opencode/plugin` (its `define` helper is an identity function).
export type SessionStatus = "idle" | "busy" | "retry"
export type SessionMessage = {
id: string
type: string
time?: { created?: unknown; completed?: unknown }
error?: unknown
text?: string
agent?: string
model?: { id: string; providerID: string; variant?: string }
content?: ReadonlyArray<unknown>
finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
interruption?: "steer"
outcome?: "succeeded" | "failed" | "interrupted"
}
export type PromptRequest = {
sessionID: string
text: string
delivery?: "steer" | "queue"
metadata?: Record<string, unknown>
}
export type ReminderHost = {
readonly directory: string
readonly projectID: string
/** Status tracked from live events; undefined when no execution event has been observed. */
status(sessionID: string): SessionStatus | undefined
session: {
get(input: { sessionID: string }): Promise<{ id: string; agent?: string } | undefined>
switchAgent(input: { sessionID: string; agent: string }): Promise<unknown>
prompt(input: PromptRequest): Promise<{ id: string; sessionID?: string } | undefined>
wait(input: { sessionID: string }): Promise<unknown>
context(input: { sessionID: string }): Promise<ReadonlyArray<SessionMessage> | undefined>
}
}
type EventLike = { type: string; data?: Record<string, unknown> }
export type PluginContextLike = {
location: { directory: string; project: { id: string } }
event: { subscribe(options?: { signal?: AbortSignal }): AsyncIterable<EventLike> }
session: ReminderHost["session"]
}
export type EventTracker = {
host: ReminderHost
stop: () => void
/** Resolves once the subscription loop has ended. */
done: Promise<void>
}
const BUSY_EVENTS = new Set(["session.execution.started"])
const IDLE_EVENTS = new Set([
"session.execution.succeeded",
"session.execution.failed",
"session.execution.interrupted",
"session.idle",
])
/**
* Build the host from a v2 plugin context. Status is derived from execution events
* because the plugin context exposes no status query; unknown sessions rely on the
* server's durable queue admission instead of an advisory pre-check.
*/
export function createHost(
ctx: PluginContextLike,
onEvent?: (event: EventLike) => void | Promise<void>,
): EventTracker {
const statuses = new Map<string, SessionStatus>()
const controller = new AbortController()
const done = (async () => {
try {
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
const sessionID = typeof event.data?.sessionID === "string" ? event.data.sessionID : undefined
if (sessionID) {
if (BUSY_EVENTS.has(event.type)) statuses.set(sessionID, "busy")
else if (IDLE_EVENTS.has(event.type)) statuses.set(sessionID, "idle")
else if (event.type === "session.status") {
const status = (event.data as { status?: { type?: string } }).status?.type
if (status === "idle" || status === "busy" || status === "retry") statuses.set(sessionID, status)
} else if (event.type === "session.deleted") statuses.delete(sessionID)
}
if (onEvent) await onEvent(event)
}
} catch (error) {
if (!controller.signal.aborted) throw error
}
})()
return {
host: {
directory: ctx.location.directory,
projectID: ctx.location.project.id,
status: (sessionID) => statuses.get(sessionID),
session: ctx.session,
},
stop: () => controller.abort(),
done,
}
}