feat(subagent): durable child catalog and list_agents
Implements the durable-subagent-catalog RFC: SubagentControlService.listChildren() enumerates a parent's direct continuable children from one sessionQuery trace, validates each child's sole subagent/descriptor event (now carrying the durable creation label), and returns one ordered SubagentListEntry[] with per-child corrupt/unsupported/unavailable diagnostics. The list_agents tool ships as a separately loadable plugin of dsh-tool-subagent-control requiring sessionQuery at load; send_message stays usable without it.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subagent/tool-subagent-control/README.md
|
||||
README.md: 5023862cba39769248a9f6cbe935d6397df39266
|
||||
README.zh.md: a5812704609edd38aedc344b4c64044fbf32c8a8
|
||||
README.md: 1fe0e49006d12b95e6c6e38cab7895238b3d3c98
|
||||
README.zh.md: 9d2d46888b0790548df35cb54613ee2334143db7
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The optional, globally named `send_message` tool: a thin adapter over `ctx.subagents.followup()`. Provider-bound `@deepseek-ai/dsh-tool-subagent` instances register distinct delegation tools per transport; this separately loaded package registers one shared follow-up tool, so multiple delegation tools never register duplicate global controls. Its presence does not determine whether a delegation tool starts continuable work.
|
||||
The optional, globally named `send_message` and `list_agents` tools are thin adapters over `ctx.subagents`. Provider-bound `@deepseek-ai/dsh-tool-subagent` instances register distinct delegation tools per transport; this separately loaded package registers shared control tools once, so multiple delegation tools never register duplicate global controls. The root plugin registers `send_message` and requires only `subagents`; the separately loadable `./list-agents` plugin registers `list_agents` and additionally requires `sessionQuery` at load. A deployment without session query keeps `send_message` and omits the list tool. Neither tool's presence determines whether a delegation tool starts continuable work.
|
||||
|
||||
The tool performs no lifecycle routing — residency and cold resume belong to the subagent service. It passes `exec.agent` as the exact live parent that authorizes delivery and attributes every message as durable provenance `{ kind: 'coordinator', senderSessionId: parent.id }`, which the service retains but never treats as authority. Every message becomes the subagent's next FIFO turn through `Agent.followup()`: if the child is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The tool forwards its execution signal, which owns admission only until inbox acceptance; once the child accepts the message the accepted turn cannot be cancelled through this tool. The child does not reply to the sender — its transcript by that id is the source of what it did. A delivery failure becomes an errored tool result stating the message was not delivered.
|
||||
|
||||
`list_agents` takes no arguments, derives the parent id from the calling agent, and renders `ctx.subagents.listChildren()`'s complete entry array without a cursor. It is discovery only: durable identity comes from each child's descriptor, while delivery-time authority and Activation ownership checks remain `send_message`'s.
|
||||
|
||||
## Model Experience
|
||||
|
||||
### Tool schema
|
||||
@@ -36,7 +38,23 @@ One short acknowledgement per call; the child's response never returns through t
|
||||
|
||||
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
||||
|
||||
### Listing result
|
||||
|
||||
#### What the model sees
|
||||
|
||||
One line per entry in the trace's stable order: `<id> [<status>] — <label>` for a child (`running` = the logical session is live, `complete` = persisted only and resumable by `send_message`), `<id> [diagnostic: <reason>]` for a candidate that could not be read (`corrupt`, `unsupported`, or `unavailable`), and `(no subagents)` for an empty result. Diagnostics never expose descriptor contents.
|
||||
|
||||
#### Token effect
|
||||
|
||||
Grows linearly with the parent's direct continuable children; there is no cursor or cap, so long-lived parents with many persisted children pay the full list each call.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Append-only; each result follows the reusable request prefix.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **A queued message has no independent result** — acceptance returns only its inbox `messageId`; the child's work on that turn lands in the durable child Session, read by its subagent id, and is neither delivered back nor collected through this tool.
|
||||
- **No steering of the current turn** — every message opens a later FIFO turn, so a message sent while the child is working runs only after its current turn finishes and cannot redirect it.
|
||||
- **Listing is a snapshot, not a delivery promise** — it may race publication, disposal, or a later message, and another process may activate a child this process reports as `complete`; cross-process accuracy requires a shared lease.
|
||||
- **No pagination or deletion** — the complete stably ordered set is returned, and persisted children remain listed for as long as their sessions remain in persistence; a service-level bound or delete operation is a later product decision.
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
可选的全局具名 `send_message` 工具:`ctx.subagents.followup()` 之上的轻量适配器。绑定提供方的 `@deepseek-ai/dsh-tool-subagent` 实例会为每种传输注册不同的委派工具;这个单独加载的包(package)只注册一个共享后续操作工具,因此多个委派工具绝不会重复注册全局控制工具。是否加载本工具不会决定委派工具是否启动可继续工作。
|
||||
可选的全局具名 `send_message` 与 `list_agents` 工具是 `ctx.subagents` 之上的轻量适配器。绑定提供方的 `@deepseek-ai/dsh-tool-subagent` 实例会为每种传输注册不同的委派工具;这个单独加载的包只注册一次共享控制工具,因此多个委派工具绝不会重复注册全局控制工具。根插件注册 `send_message`,且只要求 `subagents`;可单独加载的 `./list-agents` 插件注册 `list_agents`,并在加载时额外要求 `sessionQuery`。没有会话查询服务的部署可保留 `send_message` 并省略列表工具。是否加载这些工具不会决定委派工具是否启动可继续工作。
|
||||
|
||||
本工具不执行生命周期路由——驻留与冷恢复归 subagent 服务所有。它将 `exec.agent` 作为授权投递的准确实时父级传入,并把每条消息的来源标记为持久化来源 `{ kind: 'coordinator', senderSessionId: parent.id }`;服务会保留该来源,但绝不将其视为权限。每条消息都会通过 `Agent.followup()` 成为子 agent(智能体)的下一个 FIFO 轮次:如果子 agent 仍在工作,该消息会等待其当前轮次结束,因此无法重定向已经在进行的工作。本工具会转发其执行信号,该信号只在 inbox 接受之前掌管准入;一旦子 agent 接受消息,已接受的轮次便无法再通过本工具取消。子 agent 不会回复发送方——通过该 id 查看其 transcript 即是其所做工作的来源。投递失败会变为出错的工具结果,并明确说明消息未送达。
|
||||
|
||||
`list_agents` 不接受参数,从调用 Agent 推导 parent id,并在没有 cursor 的情况下渲染 `ctx.subagents.listChildren()` 的完整条目数组。它只负责发现:持久化身份来自每个 child 的描述符,消息送达时的鉴权和 Activation 所有权检查仍归 `send_message` 负责。
|
||||
|
||||
## 模型体验
|
||||
|
||||
### 工具 schema
|
||||
@@ -36,7 +38,23 @@
|
||||
|
||||
仅追加;新增可见内容位于可复用请求前缀之后,不会使现有 KV-cache 条目失效。
|
||||
|
||||
### 列表结果
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
每个条目按追踪结果的稳定顺序占一行:child 使用 `<id> [<status>] — <label>`(`running` 表示逻辑会话存活,`complete` 表示只存在于持久化存储中且可由 `send_message` 恢复),无法读取的候选使用 `<id> [diagnostic: <reason>]`(`corrupt`、`unsupported` 或 `unavailable`),空结果使用 `(no subagents)`。Diagnostic 绝不暴露描述符内容。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
随 parent 的直接可继续 child 数量线性增长;没有 cursor 或上限,因此长期存活且有许多持久化 child 的 parent 每次调用都会承担完整列表成本。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
仅追加;每个结果都位于可复用请求前缀之后。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
- **已排队的消息没有独立结果**:接受时只返回其 inbox `messageId`;子 agent 在该轮次的工作会落入持久化子 agent Session,按其 subagent id 读取,既不会回传,也不会通过本工具收集。
|
||||
- **不对当前轮次进行 steering**:每条消息都会开启后续 FIFO 轮次,因此在子 agent 工作时发送的消息只会在其当前轮次结束后运行,无法将其重定向。
|
||||
- **列表是快照,而非投递承诺**:它可能与发布、dispose 或后续消息发生竞态,另一个进程也可能激活当前进程报告为 `complete` 的 child;跨进程准确性需要共享租约。
|
||||
- **没有分页或删除**:系统返回完整且稳定排序的集合;只要 child 会话仍在持久化存储中,它就会继续出现在列表中,服务级上限或删除操作留待后续产品决策。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-tool-subagent-control",
|
||||
"description": "Globally named send_message tool over ctx.subagents continuations",
|
||||
"description": "Globally named send_message and list_agents tools over ctx.subagents continuations",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -15,12 +15,17 @@
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./list-agents": {
|
||||
"types": "./lib/types/list-agents.d.ts",
|
||||
"default": "./lib/types/list-agents.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
@@ -30,6 +35,7 @@
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-query": "^0.0.1",
|
||||
"@deepseek-ai/dsh-subagent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
@@ -43,9 +49,10 @@
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-query": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-query-sqlite": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent-spawn": "workspace:^",
|
||||
"@deepseek-ai/dsh-tasks": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
}
|
||||
|
||||
76
packages/subagent/tool-subagent-control/src/list-agents.ts
Normal file
76
packages/subagent/tool-subagent-control/src/list-agents.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* The globally named `list_agents` tool: a thin model-facing adapter over
|
||||
* `ctx.subagents.listChildren()`. It is separately loadable from the
|
||||
* root `send_message` plugin because it additionally requires the session
|
||||
* query service — a deployment may use `send_message` without loading session
|
||||
* query, and this plugin catches that misconfiguration at load.
|
||||
* @module @deepseek-ai/dsh-tool-subagent-control/list-agents
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-session-query'
|
||||
import type {} from '@deepseek-ai/dsh-subagent'
|
||||
|
||||
export const name = 'tool-subagent-list-agents'
|
||||
export const inject = ['tools', 'subagents', 'sessionQuery']
|
||||
|
||||
/**
|
||||
* Register the `list_agents` tool.
|
||||
* @param ctx - context carrying the tool registry, subagent service, and session query.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'list_agents',
|
||||
description:
|
||||
'List your background subagents: every subagent you started that can receive `send_message`, '
|
||||
+ 'whether it is still working (running) or has finished its current turn (complete — a follow-up '
|
||||
+ 'message starts a new turn on the same conversation). Children that could not be read are '
|
||||
+ 'reported as diagnostics instead of being silently dropped.',
|
||||
parameters: {},
|
||||
output: {
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
kind: { type: 'string', required: true, enum: ['child'] },
|
||||
id: { type: 'string', required: true },
|
||||
label: { type: 'string', required: true },
|
||||
status: { type: 'string', required: true, enum: ['running', 'complete'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
kind: { type: 'string', required: true, enum: ['diagnostic'] },
|
||||
id: { type: 'string', required: true },
|
||||
reason: { type: 'string', required: true, enum: ['corrupt', 'unsupported', 'unavailable'] },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
render: (_args, entries) => [{
|
||||
type: 'text',
|
||||
text: entries.length === 0
|
||||
? '(no subagents)'
|
||||
: entries.map(entry => entry.kind === 'child'
|
||||
? `${entry.id} [${entry.status}] — ${entry.label}`
|
||||
: `${entry.id} [diagnostic: ${entry.reason}]`).join('\n'),
|
||||
}],
|
||||
},
|
||||
async execute(_args, exec) {
|
||||
const parent = exec.agent
|
||||
if (!parent) {
|
||||
// Non-agent callers have no session whose children could be listed.
|
||||
throw new Error('list_agents requires a calling agent (exec.agent was undefined)')
|
||||
}
|
||||
return await ctx.subagents.listChildren(parent.id)
|
||||
},
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Context } from 'cordis'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import SubagentService from '@deepseek-ai/dsh-subagent'
|
||||
import type { SubagentListEntry } from '@deepseek-ai/dsh-subagent'
|
||||
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
|
||||
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
|
||||
import { TestSessionQueryService } from '../../../session-query/session-query/tests/test-service.ts'
|
||||
import * as tool from '../src/list-agents.ts'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
const roots: string[] = []
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function setup(script: ConstructorParameters<typeof MockAdapter>[0]) {
|
||||
const ctx = new Context()
|
||||
await mountAgentLoopTestDependencies(ctx)
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-tool-list-agents-'))
|
||||
roots.push(root)
|
||||
await ctx.plugin(JsonlSessionPersistence, { root })
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(SubagentService)
|
||||
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
|
||||
await ctx.plugin(TestSessionQueryService)
|
||||
await ctx.plugin(tool)
|
||||
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))
|
||||
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
|
||||
return { ctx, parent }
|
||||
}
|
||||
|
||||
function text(result: { content: { type: string; text?: string }[] }): string {
|
||||
return result.content.filter(block => block.type === 'text').map(block => block.text).join('')
|
||||
}
|
||||
|
||||
let calls = 0
|
||||
function callTool(ctx: Context, name: string, args: unknown, agent?: unknown) {
|
||||
return ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId(`call-${++calls}`),
|
||||
name,
|
||||
arguments: args,
|
||||
...agent !== undefined ? { agent: agent as never } : {},
|
||||
})
|
||||
}
|
||||
|
||||
/** Wait until a continuable child released its current Activation. */
|
||||
async function waitNoActivation(ctx: Context, childId: SessionId): Promise<void> {
|
||||
await vi.waitFor(() => {
|
||||
expect(ctx.agents.get(childId)).toBeUndefined()
|
||||
}, { timeout: 5_000 })
|
||||
}
|
||||
|
||||
describe('dsh-tool-subagent-control/list-agents', () => {
|
||||
it('registers list_agents once, globally, with no parameters', async () => {
|
||||
const { ctx } = await setup([])
|
||||
const schemas = ctx.tools.schemas().filter(schema => schema.name === 'list_agents')
|
||||
expect(schemas).toHaveLength(1)
|
||||
const props = (schemas[0]!.parameters as { properties?: Record<string, unknown> }).properties ?? {}
|
||||
expect(Object.keys(props)).toEqual([])
|
||||
expect(schemas[0]!.description).toContain('send_message')
|
||||
})
|
||||
|
||||
it('renders the empty result as (no subagents)', async () => {
|
||||
const { ctx, parent } = await setup([])
|
||||
await ctx.sessions.flush(parent.session)
|
||||
const result = await callTool(ctx, 'list_agents', {}, parent)
|
||||
expect(result.isError).toBe(false)
|
||||
expect(text(result)).toBe('(no subagents)')
|
||||
})
|
||||
|
||||
it('renders children and diagnostics in array order with the fixed text forms', async () => {
|
||||
const { ctx, parent } = await setup([textResponse('done')])
|
||||
const started = await ctx.subagents.startContinuable({
|
||||
provider: 'spawn',
|
||||
label: 'real child',
|
||||
request: { prompt: [{ type: 'text', text: 'child task' }], parent },
|
||||
signal: testToolSignal,
|
||||
})
|
||||
await waitNoActivation(ctx, started.childId)
|
||||
// Pin the render deterministically past the service: the tool is a thin
|
||||
// adapter, so its fixed text forms are what this test pins.
|
||||
const entries: SubagentListEntry[] = [
|
||||
{ kind: 'child', id: started.childId, label: 'real child', status: 'complete' },
|
||||
{ kind: 'diagnostic', id: SessionId('broken-child'), reason: 'corrupt' },
|
||||
]
|
||||
ctx.subagents.listChildren = () => Promise.resolve(entries)
|
||||
const result = await callTool(ctx, 'list_agents', {}, parent)
|
||||
expect(result.isError).toBe(false)
|
||||
expect(text(result)).toBe(
|
||||
`${started.childId} [complete] — real child\nbroken-child [diagnostic: corrupt]`,
|
||||
)
|
||||
})
|
||||
|
||||
it('lists a real settled child end-to-end with its durable label', async () => {
|
||||
const { ctx, parent } = await setup([textResponse('done')])
|
||||
const started = await ctx.subagents.startContinuable({
|
||||
provider: 'spawn',
|
||||
label: 'summarize the doc',
|
||||
request: { prompt: [{ type: 'text', text: 'child task' }], parent },
|
||||
signal: testToolSignal,
|
||||
})
|
||||
await waitNoActivation(ctx, started.childId)
|
||||
const result = await callTool(ctx, 'list_agents', {}, parent)
|
||||
expect(result.isError).toBe(false)
|
||||
expect(text(result)).toBe(`${started.childId} [complete] — summarize the doc`)
|
||||
})
|
||||
|
||||
it('fails loud when invoked without a calling agent', async () => {
|
||||
const { ctx } = await setup([])
|
||||
const result = await callTool(ctx, 'list_agents', {})
|
||||
expect(result.isError).toBe(true)
|
||||
expect(text(result)).toContain('requires a calling agent')
|
||||
})
|
||||
|
||||
it('unregisters with its plugin fiber (HMR safety)', async () => {
|
||||
const ctx = new Context()
|
||||
await mountAgentLoopTestDependencies(ctx)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(SubagentService)
|
||||
await ctx.plugin(TestSessionQueryService)
|
||||
const fiber = await ctx.plugin(tool)
|
||||
expect(ctx.tools.schemas().some(schema => schema.name === 'list_agents')).toBe(true)
|
||||
await fiber.dispose()
|
||||
expect(ctx.tools.schemas().some(schema => schema.name === 'list_agents')).toBe(false)
|
||||
})
|
||||
|
||||
it('has the namespace-plugin export shape and requires sessionQuery at load', () => {
|
||||
expect('default' in tool).toBe(false)
|
||||
expect(tool.name).toBe('tool-subagent-list-agents')
|
||||
expect(tool.inject).toEqual(['tools', 'subagents', 'sessionQuery'])
|
||||
expect(typeof tool.apply).toBe('function')
|
||||
})
|
||||
})
|
||||
@@ -82,6 +82,7 @@ describe('dsh-tool-subagent-control', () => {
|
||||
const { ctx, parent } = await setup([textResponse('first answer'), textResponse('second answer')])
|
||||
const started = await ctx.subagents.startContinuable({
|
||||
provider: 'spawn',
|
||||
label: 'child task',
|
||||
request: { prompt: [{ type: 'text', text: 'child task' }], parent },
|
||||
signal: testToolSignal,
|
||||
})
|
||||
@@ -109,6 +110,7 @@ describe('dsh-tool-subagent-control', () => {
|
||||
const { ctx, parent, adapter } = await setup([textResponse('first'), textResponse('second')])
|
||||
const started = await ctx.subagents.startContinuable({
|
||||
provider: 'spawn',
|
||||
label: 'long work',
|
||||
request: { prompt: [{ type: 'text', text: 'long work' }], parent },
|
||||
signal: testToolSignal,
|
||||
})
|
||||
@@ -144,6 +146,7 @@ describe('dsh-tool-subagent-control', () => {
|
||||
const { ctx, parent } = await setup([textResponse('first')])
|
||||
const started = await ctx.subagents.startContinuable({
|
||||
provider: 'spawn',
|
||||
label: 'child task',
|
||||
request: { prompt: [{ type: 'text', text: 'child task' }], parent },
|
||||
signal: testToolSignal,
|
||||
})
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
{
|
||||
"path": "../subagent"
|
||||
},
|
||||
{
|
||||
"path": "../../session-query/session-query"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user