| 27 | permissions: PermissionEngine; |
| 28 | cwd: string; |
| 29 | } |
| 30 | |
| 31 | export class QodexAgentRunner implements AgentRunner { |
| 32 | private map = new SessionMap(); |
| 33 | private modelByKey = new Map<string, string>(); // per-conversation model override (/model) |
| 34 | private autoByKey = new Map<string, boolean>(); // per-conversation auto-approve (/auto) |
| 35 | constructor(private deps: RunnerDeps) {} |
| 36 | |
| 37 | async reset(convKey: string): Promise<void> { |
| 38 | await this.map.clear(convKey); |
| 39 | } |
| 40 | |
| 41 | private modelFor(convKey: string): string { |
| 42 | return this.modelByKey.get(convKey) ?? this.deps.config.defaults.model; |
| 43 | } |
| 44 | |
| 45 | /** |
| 46 | * Working root for this conversation. After `/continue` or `/resume` this is |
| 47 | * the session's cwd (the CLI project), not wherever `qodex bot` was launched. |
| 48 | */ |
| 49 | private cwdFor(sessionId: string | undefined): string { |
| 50 | const sessionCwd = sessionId ? getSessionStore().loadSession(sessionId)?.meta.cwd : undefined; |
| 51 | return pickWorkingCwd({ sessionCwd, hostCwd: this.deps.cwd }); |
| 52 | } |
| 53 | |
| 54 | async status(convKey: string): Promise<RunnerStatus> { |
| 55 | const sessionId = (await this.map.get(convKey)) ?? undefined; |
| 56 | return { model: this.modelFor(convKey), cwd: this.cwdFor(sessionId), sessionId, auto: this.autoByKey.get(convKey) ?? false }; |
| 57 | } |
| 58 | |
| 59 | async setModel(convKey: string, model: string): Promise<string> { |
| 60 | this.modelByKey.set(convKey, model); |
| 61 | return model; |
| 62 | } |
| 63 | |
| 64 | async setAuto(convKey: string, on: boolean): Promise<void> { |
| 65 | this.autoByKey.set(convKey, on); |
| 66 | } |
| 67 | |
| 68 | async listSessions(convKey?: string, limit = 8): Promise<{ id: string; title: string; when: string }[]> { |
| 69 | const sid = convKey ? await this.map.get(convKey) : undefined; |
| 70 | return getSessionStore().listRecentSessions(limit, this.cwdFor(sid)).map(s => ({ |
| 71 | id: s.id, |
| 72 | title: s.title?.trim() || `${s.turn_count} turn${s.turn_count === 1 ? '' : 's'}`, |
| 73 | when: relTime(s.updated_at), |
| 74 | })); |
| 75 | } |
| 76 | |
| 77 | async listEpisodes(convKey?: string, limit = 8): Promise<{ when: string; prompt: string; summary: string }[]> { |
| 78 | const { readEpisodes } = await import('../context/episodic-memory.js'); |
| 79 | const sid = convKey ? await this.map.get(convKey) : undefined; |
| 80 | const eps = await readEpisodes(this.cwdFor(sid)); |
| 81 | return eps.slice(-limit).reverse().map(e => ({ when: relTime(e.ts), prompt: e.prompt, summary: e.summary })); |
| 82 | } |
| 83 | |
| 84 | /** Rebind a conversation to a past session by full id OR short prefix (as shown by /sessions). */ |
| 85 | async resume(convKey: string, idOrPrefix: string): Promise<boolean> { |
| 86 | const store = getSessionStore(); |
nothing calls this directly
no outgoing calls
no test coverage detected