← Public packages
@kentcdodds/devin
Start, monitor, and manage Devin sessions, knowledge, playbooks, and schedules via the Devin v3 API
src/sessions.ts
382 lines · 10.8 KB · TypeScriptimport {
clampInt,
compact,
devinOrgApi,
type Paginated,
trimString,
} from './lib/client.ts'
export type SessionStatus =
| 'RUNNING'
| 'blocked'
| 'stopped'
| 'expired'
| 'finished'
| string
export type SessionPullRequest = {
url?: string | null
title?: string | null
state?: string | null
[key: string]: unknown
}
export type Session = {
/**
* Session id returned by Devin. Pass this value as **`devinId`** to
* getSession / listMessages / sendMessage / etc. Do not rename it to
* `sessionId` in helper args — that prop is not accepted and causes 403s.
*/
session_id: string
url: string
status: SessionStatus
status_detail?: string | null
title?: string | null
tags: string[]
org_id: string
user_id?: string | null
service_user_id?: string | null
created_at: number
updated_at: number
acus_consumed: number
is_archived?: boolean | null
category?: string | null
subcategory?: string | null
origin?: string | null
devin_mode?: string | null
playbook_id?: string | null
parent_session_id?: string | null
child_session_ids?: string[] | null
structured_output?: unknown
pull_requests: SessionPullRequest[]
}
export type SessionMessage = {
event_id?: string | null
message?: string | null
source?: string | null
created_at?: number | null
}
export type SessionAttachment = {
attachment_id?: string | null
name?: string | null
content_type?: string | null
source?: string | null
url?: string | null
}
/**
* Params for helpers that operate on one session.
*
* Pass **`devinId`** — use the session's `session_id` field from create/list
* responses (e.g. `session.session_id`). There is **no** `sessionId` argument on
* these methods; using `{ sessionId }` leaves `devinId` undefined and Devin
* often responds with HTTP **403**, which looks like an auth failure but is
* usually the wrong field name.
*/
export type SessionByIdParams = {
/**
* Session id for path `/sessions/{devinId}`.
* Value is `Session.session_id` from createSession / listSessions / getSession.
* Do **not** pass `sessionId` — that prop is not accepted here.
*/
devinId: string
orgId?: string
}
/**
* Resolve `devinId` for per-session helpers. Rejects mistaken `sessionId`.
*/
export function requireDevinId(
params: SessionByIdParams & { sessionId?: unknown },
helperName: string,
): string {
if (params != null && 'sessionId' in params && (params as { sessionId?: unknown }).sessionId != null) {
throw new Error(
`${helperName}: use { devinId } (Session.session_id), not { sessionId }. ` +
`Passing sessionId is ignored and Devin typically returns HTTP 403.`,
)
}
const id = trimString(params?.devinId ?? '')
if (!id) {
throw new Error(
`${helperName}: requires { devinId: session.session_id }. ` +
`Do not pass sessionId — a 403 here is most often the wrong field name.`,
)
}
return id
}
export type ListSessionsParams = {
orgId?: string
/** Page size, 1–200 (Devin default 100). */
first?: number
/** Cursor from a previous page's `end_cursor`. */
after?: string
isArchived?: boolean
category?: string
origins?: string[]
repoNames?: string[]
sessionIds?: string[]
serviceUserIds?: string[]
userIds?: string[]
tags?: string[]
playbookId?: string
scheduleId?: string
/** Unix seconds. */
createdAfter?: number
createdBefore?: number
updatedAfter?: number
updatedBefore?: number
}
function listQuery(params: ListSessionsParams = {}) {
return compact({
first: params.first === undefined ? undefined : clampInt(params.first, 1, 200, 100),
after: params.after,
is_archived: params.isArchived,
category: params.category,
origins: params.origins,
repo_names: params.repoNames,
session_ids: params.sessionIds,
service_user_ids: params.serviceUserIds,
user_ids: params.userIds,
tags: params.tags,
playbook_id: params.playbookId,
schedule_id: params.scheduleId,
created_after: params.createdAfter,
created_before: params.createdBefore,
updated_after: params.updatedAfter,
updated_before: params.updatedBefore,
})
}
/** Cursor-paginated org sessions, newest first. */
export async function listSessions(params: ListSessionsParams = {}) {
return devinOrgApi<Paginated<Session>>({
orgId: params.orgId,
path: '/sessions',
query: listQuery(params),
})
}
/** Follow `end_cursor` until `maxPages` or the last page. */
export async function listAllSessions(
params: ListSessionsParams & { maxPages?: number } = {},
) {
const maxPages = clampInt(params.maxPages ?? 5, 1, 20, 5)
const items: Session[] = []
let after = params.after
let pages = 0
while (pages < maxPages) {
const page = await listSessions({ ...params, after })
items.push(...page.items)
pages += 1
if (!page.has_next_page || !page.end_cursor) {
return { items, pages, truncated: false, end_cursor: page.end_cursor ?? null }
}
after = page.end_cursor
}
return { items, pages, truncated: true, end_cursor: after ?? null }
}
export type CreateSessionParams = {
orgId?: string
prompt: string
title?: string
tags?: string[]
repos?: string[]
playbookId?: string
childPlaybookId?: string
knowledgeIds?: string[]
secretIds?: string[]
attachmentUrls?: string[]
sessionLinks?: string[]
/** Requires ImpersonateOrgSessions on the service user's role. */
createAsUserId?: string
devinMode?: string
platform?: string
maxAcuLimit?: number
bypassApproval?: boolean
resumable?: boolean
structuredOutputRequired?: boolean
structuredOutputSchema?: Record<string, unknown>
sessionSecrets?: Array<Record<string, unknown>>
}
/** Start a session. Sessions consume ACUs — confirm intent before calling. */
export async function createSession(params: CreateSessionParams) {
const prompt = trimString(params.prompt)
if (!prompt) throw new Error('createSession requires a non-empty prompt')
return devinOrgApi<Session>({
orgId: params.orgId,
path: '/sessions',
method: 'POST',
body: compact({
prompt,
title: params.title,
tags: params.tags,
repos: params.repos,
playbook_id: params.playbookId,
child_playbook_id: params.childPlaybookId,
knowledge_ids: params.knowledgeIds,
secret_ids: params.secretIds,
attachment_urls: params.attachmentUrls,
session_links: params.sessionLinks,
create_as_user_id: params.createAsUserId,
devin_mode: params.devinMode,
platform: params.platform,
max_acu_limit: params.maxAcuLimit,
bypass_approval: params.bypassApproval,
resumable: params.resumable,
structured_output_required: params.structuredOutputRequired,
structured_output_schema: params.structuredOutputSchema,
session_secrets: params.sessionSecrets,
}),
})
}
/**
* Load one session by **`devinId`** (`Session.session_id`).
* @example await getSession({ devinId: session.session_id })
* // not getSession({ sessionId: ... }) — that yields 403
*/
export async function getSession(params: SessionByIdParams) {
const devinId = requireDevinId(params, 'getSession')
return devinOrgApi<Session>({
orgId: params.orgId,
path: `/sessions/${encodeURIComponent(devinId)}`,
})
}
/** Session status plus the fields worth surfacing in a status report. Uses `devinId` only. */
export async function getSessionStatus(params: SessionByIdParams) {
const session = await getSession(params)
return {
session_id: session.session_id,
title: session.title ?? null,
status: session.status,
status_detail: session.status_detail ?? null,
url: session.url,
acus_consumed: session.acus_consumed,
updated_at: session.updated_at,
is_archived: session.is_archived ?? null,
pull_requests: session.pull_requests,
structured_output: session.structured_output ?? null,
}
}
/**
* List messages for a session. Pass **`devinId`** (`Session.session_id`), not `sessionId`.
* @example await listMessages({ devinId: '…' })
*/
export async function listMessages(
params: SessionByIdParams & { first?: number; after?: string },
) {
const devinId = requireDevinId(params, 'listMessages')
return devinOrgApi<Paginated<SessionMessage>>({
orgId: params.orgId,
path: `/sessions/${encodeURIComponent(devinId)}/messages`,
query: compact({
first: params.first === undefined ? undefined : clampInt(params.first, 1, 200, 50),
after: params.after,
}),
})
}
/** Send a message to a running session (queues if the session is busy). */
export async function sendMessage(
params: SessionByIdParams & {
message: string
attachmentUrls?: string[]
/** Requires ImpersonateOrgSessions on the service user's role. */
messageAsUserId?: string
},
) {
const devinId = requireDevinId(params, 'sendMessage')
const message = trimString(params.message)
if (!message) throw new Error('sendMessage requires a non-empty message')
return devinOrgApi<unknown>({
orgId: params.orgId,
path: `/sessions/${encodeURIComponent(devinId)}/messages`,
method: 'POST',
body: compact({
message,
attachment_urls: params.attachmentUrls,
message_as_user_id: params.messageAsUserId,
}),
})
}
/** Terminate a session's VM. Destructive — the session cannot be resumed. */
export async function terminateSession(
params: SessionByIdParams & {
/** Also archive the session as part of terminating it. */
archive?: boolean
},
) {
const devinId = requireDevinId(params, 'terminateSession')
return devinOrgApi<unknown>({
orgId: params.orgId,
path: `/sessions/${encodeURIComponent(devinId)}`,
method: 'DELETE',
query: compact({ archive: params.archive }),
})
}
export async function archiveSession(params: SessionByIdParams) {
const devinId = requireDevinId(params, 'archiveSession')
return devinOrgApi<unknown>({
orgId: params.orgId,
path: `/sessions/${encodeURIComponent(devinId)}/archive`,
method: 'POST',
})
}
export async function unarchiveSession(params: SessionByIdParams) {
const devinId = requireDevinId(params, 'unarchiveSession')
return devinOrgApi<unknown>({
orgId: params.orgId,
path: `/sessions/${encodeURIComponent(devinId)}/unarchive`,
method: 'POST',
})
}
export async function getSessionTags(params: SessionByIdParams) {
const devinId = requireDevinId(params, 'getSessionTags')
return devinOrgApi<{ tags: string[] }>({
orgId: params.orgId,
path: `/sessions/${encodeURIComponent(devinId)}/tags`,
})
}
/** `mode: 'replace'` (PUT) overwrites tags; `'add'` (POST) appends them. */
export async function setSessionTags(
params: SessionByIdParams & {
tags: string[]
mode?: 'replace' | 'add'
},
) {
const devinId = requireDevinId(params, 'setSessionTags')
return devinOrgApi<{ tags: string[] }>({
orgId: params.orgId,
path: `/sessions/${encodeURIComponent(devinId)}/tags`,
method: params.mode === 'add' ? 'POST' : 'PUT',
body: { tags: params.tags },
})
}
export async function listSessionAttachments(params: SessionByIdParams) {
const devinId = requireDevinId(params, 'listSessionAttachments')
return devinOrgApi<{ items: SessionAttachment[] } | SessionAttachment[]>({
orgId: params.orgId,
path: `/sessions/${encodeURIComponent(devinId)}/attachments`,
})
}
export default listSessions