-
Notifications
You must be signed in to change notification settings - Fork 8
Named sessions #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Named sessions #42
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -321,6 +321,11 @@ ${agentList} | |||||||||
|
|
||||||||||
| If omitted, continues with current agent. | ||||||||||
|
|
||||||||||
| NAME PARAMETER (optional): | ||||||||||
|
|
||||||||||
| Custom name for new or forked sessions. Helps identify sessions in the session list. | ||||||||||
| Only used in 'new' and 'fork' modes. Ignored in 'message' and 'compact' modes. | ||||||||||
|
|
||||||||||
| EXAMPLES: | ||||||||||
|
|
||||||||||
| # COLLABORATE: Multi-agent problem solving | ||||||||||
|
|
@@ -331,13 +336,14 @@ EXAMPLES: | |||||||||
| }) | ||||||||||
| # Plan reviews, responds, can pass back to build | ||||||||||
|
|
||||||||||
| # HANDOFF: Clean phase transition | ||||||||||
| # HANDOFF: Clean phase transition with named session | ||||||||||
| session({ | ||||||||||
| mode: "new", | ||||||||||
| agent: "researcher", | ||||||||||
| name: "API Research", | ||||||||||
| text: "Research best practices for API design" | ||||||||||
| }) | ||||||||||
| # Fresh session, no baggage from previous implementation work | ||||||||||
| # Fresh session titled "API Research", no baggage from previous work | ||||||||||
|
|
||||||||||
| # COMPRESS: Long conversation with handoff | ||||||||||
| session({ | ||||||||||
|
|
@@ -347,18 +353,20 @@ EXAMPLES: | |||||||||
| }) | ||||||||||
| # Compacts history, adds handoff context, plan agent responds | ||||||||||
|
|
||||||||||
| # PARALLELIZE: Try multiple approaches | ||||||||||
| # PARALLELIZE: Named forked sessions for comparison | ||||||||||
| session({ | ||||||||||
| mode: "fork", | ||||||||||
| agent: "build", | ||||||||||
| agent: "plan", | ||||||||||
| name: "Redux Architecture", | ||||||||||
| text: "Implement using Redux" | ||||||||||
| }) | ||||||||||
| session({ | ||||||||||
| mode: "fork", | ||||||||||
| agent: "build", | ||||||||||
| agent: "plan", | ||||||||||
| name: "Context API Architecture", | ||||||||||
| text: "Implement using Context API" | ||||||||||
| }) | ||||||||||
| # Two independent sessions, compare results | ||||||||||
| # Two named sessions for easy identification, compare results | ||||||||||
| `, | ||||||||||
|
|
||||||||||
| args: { | ||||||||||
|
|
@@ -374,6 +382,12 @@ EXAMPLES: | |||||||||
| .describe( | ||||||||||
| "Primary agent name (e.g., 'build', 'plan') for agent switching", | ||||||||||
| ), | ||||||||||
| name: tool.schema | ||||||||||
| .string() | ||||||||||
| .optional() | ||||||||||
| .describe( | ||||||||||
| "Custom name for the session (used in 'new' and 'fork' modes)", | ||||||||||
| ), | ||||||||||
| }, | ||||||||||
|
|
||||||||||
| async execute(args, toolCtx) { | ||||||||||
|
|
@@ -388,11 +402,12 @@ EXAMPLES: | |||||||||
|
|
||||||||||
| case "new": | ||||||||||
| // Create session via SDK for agent control | ||||||||||
| const sessionTitle = args.name || (args.agent | ||||||||||
| ? `Session via ${args.agent}` | ||||||||||
| : "New session") | ||||||||||
| const newSession = await ctx.client.session.create({ | ||||||||||
| body: { | ||||||||||
| title: args.agent | ||||||||||
| ? `Session via ${args.agent}` | ||||||||||
| : "New session", | ||||||||||
| title: sessionTitle, | ||||||||||
| }, | ||||||||||
| }) | ||||||||||
|
|
||||||||||
|
|
@@ -405,7 +420,12 @@ EXAMPLES: | |||||||||
| }, | ||||||||||
| }) | ||||||||||
|
|
||||||||||
| return `New session created with ${args.agent || "build"} agent (ID: ${newSession.data.id})` | ||||||||||
| const sanitizeForPrompt = (str: string) => str.replace(/\n/g, " ") | ||||||||||
| const safeName = args.name ? sanitizeForPrompt(args.name) : null | ||||||||||
| const safeAgent = sanitizeForPrompt(args.agent || "build") | ||||||||||
| return safeName | ||||||||||
| ? `New session "${safeName}" created with ${safeAgent} agent (ID: ${newSession.data.id})` | ||||||||||
| : `New session created with ${safeAgent} agent (ID: ${newSession.data.id})` | ||||||||||
|
|
||||||||||
| case "compact": | ||||||||||
| try { | ||||||||||
|
|
@@ -470,6 +490,13 @@ EXAMPLES: | |||||||||
| body: {}, | ||||||||||
| }) | ||||||||||
|
|
||||||||||
| if (args.name) { | ||||||||||
| await ctx.client.session.update({ | ||||||||||
| path: { id: forkedSession.data.id }, | ||||||||||
| body: { title: args.name }, | ||||||||||
| }) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Send new message in forked session | ||||||||||
| await ctx.client.session.prompt({ | ||||||||||
| path: { id: forkedSession.data.id }, | ||||||||||
|
|
@@ -479,7 +506,12 @@ EXAMPLES: | |||||||||
| }, | ||||||||||
| }) | ||||||||||
|
|
||||||||||
| return `Forked session with ${args.agent || "build"} agent - history preserved (ID: ${forkedSession.data.id})` | ||||||||||
| const sanitizeFork = (str: string) => str.replace(/\n/g, " ") | ||||||||||
| const forkSafeName = args.name ? sanitizeFork(args.name) : null | ||||||||||
| const forkSafeAgent = sanitizeFork(args.agent || "build") | ||||||||||
| return forkSafeName | ||||||||||
| ? `Forked session "${forkSafeName}" with ${forkSafeAgent} agent - history preserved (ID: ${forkedSession.data.id})` | ||||||||||
| : `Forked session with ${forkSafeAgent} agent - history preserved (ID: ${forkedSession.data.id})` | ||||||||||
| } | ||||||||||
| } catch (error) { | ||||||||||
| const message = | ||||||||||
|
|
@@ -498,6 +530,102 @@ EXAMPLES: | |||||||||
| } | ||||||||||
| }, | ||||||||||
| }), | ||||||||||
|
|
||||||||||
| session_list: tool({ | ||||||||||
| description: `List all OpenCode sessions with optional filtering. | ||||||||||
|
|
||||||||||
| Returns a list of available sessions with metadata including title and creation date. | ||||||||||
|
|
||||||||||
| Arguments: | ||||||||||
| - limit (optional): Maximum number of sessions to return | ||||||||||
| - from_date (optional): Filter sessions from this date (ISO 8601 format) | ||||||||||
| - to_date (optional): Filter sessions until this date (ISO 8601 format) | ||||||||||
|
|
||||||||||
| Example output: | ||||||||||
| | Session ID | Title | Created | | ||||||||||
| |------------|-------|---------| | ||||||||||
| | ses_abc123 | API Research | 2026-02-21 | | ||||||||||
| | ses_def456 | Auth Implementation | 2026-02-20 |`, | ||||||||||
|
|
||||||||||
| args: { | ||||||||||
| limit: tool.schema | ||||||||||
| .number() | ||||||||||
| .optional() | ||||||||||
| .describe("Maximum number of sessions to return"), | ||||||||||
| from_date: tool.schema | ||||||||||
| .string() | ||||||||||
| .optional() | ||||||||||
| .describe("Filter sessions from this date (ISO 8601 format)"), | ||||||||||
| to_date: tool.schema | ||||||||||
| .string() | ||||||||||
| .optional() | ||||||||||
| .describe("Filter sessions until this date (ISO 8601 format)"), | ||||||||||
| }, | ||||||||||
|
|
||||||||||
| async execute(args) { | ||||||||||
| try { | ||||||||||
| const response = await ctx.client.session.list() | ||||||||||
|
|
||||||||||
| if (!response.data) { | ||||||||||
| return "No sessions found." | ||||||||||
| } | ||||||||||
|
|
||||||||||
| let sessions = response.data | ||||||||||
|
|
||||||||||
| if (args.from_date) { | ||||||||||
| const fromDate = new Date(args.from_date).getTime() | ||||||||||
| sessions = sessions.filter((s) => { | ||||||||||
| const created = s.time?.created | ||||||||||
| ? new Date(s.time.created).getTime() | ||||||||||
| : 0 | ||||||||||
| return created >= fromDate | ||||||||||
| }) | ||||||||||
|
Comment on lines
+577
to
+582
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a session lacks a sessions = sessions.filter((s) => {
if (!s.time?.created) {
return false;
}
const created = new Date(s.time.created).getTime();
return created >= fromDate;
}) |
||||||||||
| } | ||||||||||
|
|
||||||||||
| if (args.to_date) { | ||||||||||
| const toDate = new Date(args.to_date) | ||||||||||
| toDate.setUTCHours(23, 59, 59, 999) | ||||||||||
| const toTimestamp = toDate.getTime() | ||||||||||
| sessions = sessions.filter((s) => { | ||||||||||
| const created = s.time?.created | ||||||||||
| ? new Date(s.time.created).getTime() | ||||||||||
| : 0 | ||||||||||
| return created <= toTimestamp | ||||||||||
| }) | ||||||||||
|
Comment on lines
+589
to
+594
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the sessions = sessions.filter((s) => {
if (!s.time?.created) {
return false;
}
const created = new Date(s.time.created).getTime();
return created <= toTimestamp;
}) |
||||||||||
| } | ||||||||||
|
|
||||||||||
| if (args.limit && args.limit > 0) { | ||||||||||
| sessions = sessions.slice(0, args.limit) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (sessions.length === 0) { | ||||||||||
| return "No sessions found matching criteria." | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const header = | ||||||||||
| "| Session ID | Title | Created |" | ||||||||||
| const separator = | ||||||||||
| "|------------|-------|---------|" | ||||||||||
|
|
||||||||||
| const rows = sessions.map((s) => { | ||||||||||
| const sanitizeForMarkdown = (str: string) => | ||||||||||
| str.replace(/\|/g, "\\|").replace(/\n/g, " ") | ||||||||||
|
Comment on lines
+611
to
+612
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||
| const id = sanitizeForMarkdown(s.id || "unknown") | ||||||||||
| const title = sanitizeForMarkdown(s.title || "Untitled") | ||||||||||
| const created = s.time?.created | ||||||||||
| ? new Date(s.time.created).toISOString().split("T")[0] | ||||||||||
| : "N/A" | ||||||||||
| return `| ${id} | ${title} | ${created} |` | ||||||||||
| }) | ||||||||||
|
|
||||||||||
| return [header, separator, ...rows].join("\n") | ||||||||||
| } catch (error) { | ||||||||||
| const message = | ||||||||||
| error instanceof Error ? error.message : String(error) | ||||||||||
| return `Error listing sessions: ${message}` | ||||||||||
| } | ||||||||||
| }, | ||||||||||
| }), | ||||||||||
| }, | ||||||||||
| } | ||||||||||
| } | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This
sanitizeForkfunction is identical tosanitizeForPromptdefined earlier in thecase "new"block (line 423). To improve maintainability and reduce redundancy, you could define this function once in a higher scope (e.g., before theswitchstatement) and reuse it in both places.