@obtoai/agent-bridge 0.1.0-beta.1 → 0.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -9
- package/cli/status.js +28 -15
- package/package.json +1 -1
- package/src/daemon.js +31 -20
- package/src/driver.js +52 -28
- package/src/state.js +64 -8
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A local daemon that lets a coding agent — [Claude Code](https://claude.ai/code) or [OpenAI Codex](https://developers.openai.com/codex) — running on your machine be driven from the [OBTO Agent Bridge](https://obto.co) web UI, even when you're away from the keyboard.
|
|
4
4
|
|
|
5
|
-
You post a message on a thread from your phone or laptop. The daemon (running on your
|
|
5
|
+
You post a message on a thread from your phone or laptop. The daemon (running on your machine, no port forwarding required) receives it over a long-lived HTTPS stream, spawns or resumes an agent session in your project directory, and the response posts back to the bridge thread within seconds.
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ You post a message on a thread from your phone or laptop. The daemon (running on
|
|
|
10
10
|
|
|
11
11
|
## What you'll need
|
|
12
12
|
|
|
13
|
-
- macOS or
|
|
13
|
+
- macOS, Linux, or Windows, **Node.js 18.17+**
|
|
14
14
|
- One coding agent installed, with your own auth:
|
|
15
15
|
- **Claude** — Claude Code / the Claude Agent SDK, billed to your Anthropic account; or
|
|
16
16
|
- **Codex** — the `codex` CLI (`npm i -g @openai/codex`), signed in to your OpenAI/ChatGPT account
|
|
@@ -65,7 +65,7 @@ Now open the bridge UI in any browser, log in with the browser credentials from
|
|
|
65
65
|
- Reply on an existing thread — daemon resumes the session bound to that thread
|
|
66
66
|
- Start a new thread via the **+ New thread** button — daemon spawns a fresh session in your project directory
|
|
67
67
|
|
|
68
|
-
Within ~5–10 seconds you should see
|
|
68
|
+
Within ~5–10 seconds you should see the agent's reply appear back on the thread.
|
|
69
69
|
|
|
70
70
|
## Other commands
|
|
71
71
|
|
|
@@ -78,8 +78,8 @@ Within ~5–10 seconds you should see Claude's reply appear back on the thread.
|
|
|
78
78
|
## How it actually works
|
|
79
79
|
|
|
80
80
|
```
|
|
81
|
-
Your phone OBTO server Your
|
|
82
|
-
───────── ───────────
|
|
81
|
+
Your phone OBTO server Your machine
|
|
82
|
+
───────── ─────────── ────────────
|
|
83
83
|
[reply form] ──► /api/reply ─► Mongo (durable)
|
|
84
84
|
└─► RabbitMQ (publish bridge.<acct>.reply.<thread>)
|
|
85
85
|
◄── /api/bridge/stream (SSE, Bearer auth)
|
|
@@ -103,11 +103,15 @@ Key bits:
|
|
|
103
103
|
|
|
104
104
|
The daemon runs your chosen agent on your machine with **your** credentials — Anthropic for `claude` (whatever Claude Code uses: `ANTHROPIC_API_KEY` or your Claude.ai session), or your OpenAI/ChatGPT account for `codex`. Every bridge-driven turn is a normal API call billed to you. We don't proxy.
|
|
105
105
|
|
|
106
|
-
##
|
|
106
|
+
## Data handling
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
**Your model traffic never touches us.** The daemon runs on your machine and calls Anthropic or OpenAI with *your own* credentials. Your prompts, your code, and the model's responses pass directly between your machine and the model provider, under your own API account and its terms. OBTO does not proxy, route, or see that traffic.
|
|
109
|
+
|
|
110
|
+
**What the bridge stores.** For threads to work, the messages you and the agent post are saved in OBTO's database — that's what makes a thread durable and readable from your phone. Threads are strictly scoped to your account; one tenant can never see another's. Your daemon's API token is stored server-side only as a SHA-256 hash; the plaintext token never leaves your local config file.
|
|
111
|
+
|
|
112
|
+
**What we don't do with it.** OBTO does not use your bridge messages to train models, and does not sell or share your data with third parties.
|
|
113
|
+
|
|
114
|
+
**Deletion.** Email `support@obto.co` to delete your account and every message associated with it.
|
|
111
115
|
|
|
112
116
|
## License
|
|
113
117
|
|
package/cli/status.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// `obto-bridge status` — read local state.json and print thread→session
|
|
4
|
-
// Read-only
|
|
3
|
+
// `obto-bridge status` — read local state.json and print thread→session
|
|
4
|
+
// bindings. Read-only. v1.1: a thread keeps one session per agent.
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
@@ -29,20 +29,33 @@ if (threads.length === 0) {
|
|
|
29
29
|
const fmtAge = (iso) => {
|
|
30
30
|
if (!iso) return '?';
|
|
31
31
|
const ms = Date.now() - new Date(iso).getTime();
|
|
32
|
-
if (ms <
|
|
33
|
-
if (ms <
|
|
34
|
-
if (ms <
|
|
35
|
-
return Math.floor(ms /
|
|
32
|
+
if (ms < 60000) return Math.floor(ms / 1000) + 's ago';
|
|
33
|
+
if (ms < 3600000) return Math.floor(ms / 60000) + 'm ago';
|
|
34
|
+
if (ms < 86400000) return Math.floor(ms / 3600000) + 'h ago';
|
|
35
|
+
return Math.floor(ms / 86400000) + 'd ago';
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
console.log('Thread Session ID Last drive');
|
|
39
|
-
console.log('-'.repeat(
|
|
38
|
+
console.log('Thread Agent Session ID Last drive');
|
|
39
|
+
console.log('-'.repeat(100));
|
|
40
40
|
threads.forEach((t) => {
|
|
41
|
-
const b = bindings[t];
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
const b = bindings[t] || {};
|
|
42
|
+
// v1.1 per-agent sessions; tolerate a stray un-migrated v1 flat binding.
|
|
43
|
+
const sessions = b.sessions && typeof b.sessions === 'object'
|
|
44
|
+
? b.sessions
|
|
45
|
+
: (b.sessionId ? { claude: b } : {});
|
|
46
|
+
const agents = Object.keys(sessions);
|
|
47
|
+
if (agents.length === 0) {
|
|
48
|
+
console.log(t.padEnd(36) + ' (no session yet)');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
agents.forEach((agent, i) => {
|
|
52
|
+
const s = sessions[agent] || {};
|
|
53
|
+
const sid = String(s.sessionId || '').slice(0, 36);
|
|
54
|
+
console.log(
|
|
55
|
+
(i === 0 ? t : '').padEnd(36) + ' ' +
|
|
56
|
+
agent.padEnd(7) + ' ' +
|
|
57
|
+
sid.padEnd(38) + ' ' +
|
|
58
|
+
fmtAge(s.lastDriveAt),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
44
61
|
});
|
|
45
|
-
console.log('');
|
|
46
|
-
console.log('JSONLs at: ' + (bindings[threads[0]] && bindings[threads[0]].projectDir
|
|
47
|
-
? '~/.claude/projects/' + bindings[threads[0]].projectDir.replace(/\//g, '-') + '/'
|
|
48
|
-
: 'unknown'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@obtoai/agent-bridge",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.3",
|
|
4
4
|
"description": "Local consumer for the OBTO Agent Bridge. Receives bridge events over SSE and drives a coding agent (Claude Code or OpenAI Codex) on your machine.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "OBTO Inc.",
|
package/src/daemon.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { loadConfig } = require('./config');
|
|
4
4
|
const { startStream } = require('./stream-client');
|
|
5
|
-
const { loadState, saveState,
|
|
6
|
-
const { drive, tryResolvePermission,
|
|
5
|
+
const { loadState, saveState, getAgentSession, setAgentSession } = require('./state');
|
|
6
|
+
const { drive, tryResolvePermission, agentFor } = require('./driver');
|
|
7
7
|
const { postAgentActivity } = require('./bridge-http');
|
|
8
8
|
|
|
9
9
|
const log = (level, msg, data) => {
|
|
@@ -67,12 +67,15 @@ const handleEvent = async (sseEvent) => {
|
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
// v1.1 — which agent this thread is bound to (server-set, on the event).
|
|
71
|
+
const agent = agentFor(payload);
|
|
72
|
+
const session = getAgentSession(state, threadId, agent);
|
|
71
73
|
log('event', 'reply received', {
|
|
72
74
|
threadId,
|
|
75
|
+
agent,
|
|
73
76
|
author: payload.author,
|
|
74
77
|
messageId: payload.messageId,
|
|
75
|
-
|
|
78
|
+
hasSession: !!session,
|
|
76
79
|
});
|
|
77
80
|
|
|
78
81
|
// Permission-relay replies: resolve the pending request inside the driver and
|
|
@@ -85,10 +88,13 @@ const handleEvent = async (sseEvent) => {
|
|
|
85
88
|
// indicator drops whether the turn succeeds, skips, or throws.
|
|
86
89
|
emitActivity(threadId, 'working');
|
|
87
90
|
try {
|
|
91
|
+
// The driver receives a flat per-agent session as `binding` — the v1
|
|
92
|
+
// driver contract is unchanged. v1.1 just keeps one such session per
|
|
93
|
+
// agent on the thread, so a claude<->codex switch resumes each side.
|
|
88
94
|
const result = await drive({
|
|
89
95
|
threadId,
|
|
90
96
|
projectDir: cfg.projectDir,
|
|
91
|
-
binding,
|
|
97
|
+
binding: session,
|
|
92
98
|
payload,
|
|
93
99
|
log,
|
|
94
100
|
});
|
|
@@ -96,22 +102,27 @@ const handleEvent = async (sseEvent) => {
|
|
|
96
102
|
if (
|
|
97
103
|
result &&
|
|
98
104
|
result.sessionId &&
|
|
99
|
-
(!
|
|
105
|
+
(!session || session.sessionId !== result.sessionId)
|
|
100
106
|
) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
setAgentSession(
|
|
108
|
+
state,
|
|
109
|
+
threadId,
|
|
110
|
+
agent,
|
|
111
|
+
{
|
|
112
|
+
sessionId: result.sessionId,
|
|
113
|
+
projectDir: result.projectDir,
|
|
114
|
+
jsonlPath: result.jsonlPath,
|
|
115
|
+
lastJsonlMtimeMs: result.lastJsonlMtimeMs || null,
|
|
116
|
+
createdAt: new Date().toISOString(),
|
|
117
|
+
lastDriveAt: new Date().toISOString(),
|
|
118
|
+
},
|
|
119
|
+
{ agentId: cfg.agentId },
|
|
120
|
+
);
|
|
121
|
+
log('info', 'session bound', { threadId, agent, sessionId: result.sessionId });
|
|
122
|
+
} else if (session && !result.skipped) {
|
|
123
|
+
session.lastDriveAt = new Date().toISOString();
|
|
113
124
|
if (result && result.lastJsonlMtimeMs) {
|
|
114
|
-
|
|
125
|
+
session.lastJsonlMtimeMs = result.lastJsonlMtimeMs;
|
|
115
126
|
}
|
|
116
127
|
saveState(state);
|
|
117
128
|
}
|
|
@@ -130,7 +141,7 @@ const start = () => {
|
|
|
130
141
|
baseUrl: cfg.baseUrl,
|
|
131
142
|
accountId: cfg.accountId,
|
|
132
143
|
agentId: cfg.agentId,
|
|
133
|
-
|
|
144
|
+
agents: ['claude', 'codex'],
|
|
134
145
|
projectDir: cfg.projectDir,
|
|
135
146
|
boundThreads: Object.keys(state.bindings || {}),
|
|
136
147
|
});
|
package/src/driver.js
CHANGED
|
@@ -1,47 +1,71 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
// operator configured — Claude (via the Claude Agent SDK) or Codex (via the
|
|
5
|
-
// Codex SDK) — chosen by `agent` in config.json / the BRIDGE_AGENT env var.
|
|
6
|
-
// Everything else in the daemon (SSE, state, the bridge HTTP client) is
|
|
7
|
-
// agent-neutral; only the driver differs.
|
|
3
|
+
// Dual-driver dispatch (v1.1).
|
|
8
4
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
5
|
+
// v1 resolved ONE agent at startup and drove only that. v1.1 makes the daemon
|
|
6
|
+
// agent-agnostic per event: it can drive BOTH Claude (Claude Agent SDK) and
|
|
7
|
+
// Codex (Codex SDK), and routes each bridge event to the right driver by the
|
|
8
|
+
// thread's `agent` field (`payload.agent`, set server-side from the thread's
|
|
9
|
+
// routing record).
|
|
10
|
+
//
|
|
11
|
+
// Drivers are require()d lazily and cached — a machine that only ever runs
|
|
12
|
+
// Claude threads never pays to load @openai/codex-sdk, and vice versa.
|
|
11
13
|
|
|
12
14
|
const { loadConfig } = require('./config');
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
const KNOWN_AGENTS = ['claude', 'codex'];
|
|
17
|
+
|
|
18
|
+
const cache = {};
|
|
15
19
|
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
18
|
-
|
|
20
|
+
const loadDriver = (name) => {
|
|
21
|
+
if (cache[name]) return cache[name];
|
|
22
|
+
const mod = name === 'codex'
|
|
23
|
+
? require('./codex-driver')
|
|
24
|
+
: require('./claude-driver');
|
|
25
|
+
cache[name] = mod;
|
|
26
|
+
return mod;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Fallback agent for events that arrive without an explicit `agent` — an
|
|
30
|
+
// older bridge, or a thread created before v1.1. Reads config.agent (the v1
|
|
31
|
+
// init choice), else 'claude'.
|
|
32
|
+
let fallbackAgent = null;
|
|
33
|
+
const getFallbackAgent = () => {
|
|
34
|
+
if (fallbackAgent) return fallbackAgent;
|
|
35
|
+
let a = 'claude';
|
|
19
36
|
try {
|
|
20
|
-
|
|
37
|
+
a = (loadConfig().agent || 'claude').toLowerCase();
|
|
21
38
|
} catch (_) {
|
|
22
39
|
// config unreadable — default to claude
|
|
23
40
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} else {
|
|
27
|
-
resolved = { name: 'claude', mod: require('./claude-driver') };
|
|
28
|
-
}
|
|
29
|
-
return resolved;
|
|
41
|
+
fallbackAgent = a === 'codex' ? 'codex' : 'claude';
|
|
42
|
+
return fallbackAgent;
|
|
30
43
|
};
|
|
31
44
|
|
|
32
|
-
|
|
45
|
+
// Resolve which agent a bridge event targets.
|
|
46
|
+
const agentFor = (payload) => {
|
|
47
|
+
const a = payload && payload.agent ? String(payload.agent).toLowerCase() : '';
|
|
48
|
+
if (KNOWN_AGENTS.indexOf(a) !== -1) return a;
|
|
49
|
+
return getFallbackAgent();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Drive one bridge event with the agent its thread is bound to.
|
|
53
|
+
const drive = (params) => {
|
|
54
|
+
const name = agentFor(params && params.payload);
|
|
55
|
+
return loadDriver(name).drive(params);
|
|
56
|
+
};
|
|
33
57
|
|
|
34
|
-
// Permission relay is Claude-only — Codex exposes no per-tool callback.
|
|
35
|
-
//
|
|
36
|
-
//
|
|
58
|
+
// Permission relay is Claude-only — the Codex SDK exposes no per-tool callback.
|
|
59
|
+
// We delegate to the claude driver's resolver regardless of the thread's agent:
|
|
60
|
+
// it keys on threadId against its own pending-request map, so a codex thread
|
|
61
|
+
// (which never has a pending claude request) simply returns false and the
|
|
62
|
+
// reply falls through to drive().
|
|
37
63
|
const tryResolvePermission = (threadId, body, log) => {
|
|
38
|
-
const
|
|
39
|
-
if (typeof
|
|
40
|
-
return
|
|
64
|
+
const claude = loadDriver('claude');
|
|
65
|
+
if (typeof claude.tryResolvePermission === 'function') {
|
|
66
|
+
return claude.tryResolvePermission(threadId, body, log);
|
|
41
67
|
}
|
|
42
68
|
return false;
|
|
43
69
|
};
|
|
44
70
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
module.exports = { drive, tryResolvePermission, activeAgent };
|
|
71
|
+
module.exports = { drive, tryResolvePermission, agentFor, KNOWN_AGENTS };
|
package/src/state.js
CHANGED
|
@@ -13,18 +13,48 @@ const ensureDir = () => {
|
|
|
13
13
|
} catch (_) {}
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
// Schema:
|
|
16
|
+
// Schema (v1.1 — per-agent sessions):
|
|
17
17
|
// {
|
|
18
18
|
// "bindings": {
|
|
19
19
|
// "<threadId>": {
|
|
20
|
-
// "sessionId": "<uuid>",
|
|
21
|
-
// "projectDir": "/abs/path",
|
|
22
|
-
// "jsonlPath": "/.../<sid>.jsonl",
|
|
23
20
|
// "createdAt": "iso-ts",
|
|
24
|
-
// "
|
|
21
|
+
// "agentId": "<machine id>",
|
|
22
|
+
// "sessions": {
|
|
23
|
+
// "claude": { "sessionId", "projectDir", "jsonlPath", "lastJsonlMtimeMs", "lastDriveAt" },
|
|
24
|
+
// "codex": { "sessionId", "projectDir", "lastDriveAt" }
|
|
25
|
+
// }
|
|
25
26
|
// }, ...
|
|
26
27
|
// }
|
|
27
28
|
// }
|
|
29
|
+
// A thread keeps one session PER agent, so switching claude<->codex and back
|
|
30
|
+
// resumes each engine's own context. v1 flat bindings are migrated on load.
|
|
31
|
+
|
|
32
|
+
// Migrate a v1 flat binding ({ sessionId, projectDir, ... }) into the v1.1
|
|
33
|
+
// per-agent shape. The flat session is filed under 'claude' — the v1 default
|
|
34
|
+
// agent. A v1 codex daemon's threads simply first-touch codex fresh after the
|
|
35
|
+
// upgrade, which is acceptable (a switch is a fresh first-touch anyway).
|
|
36
|
+
const migrateBinding = (b) => {
|
|
37
|
+
if (!b || typeof b !== 'object') {
|
|
38
|
+
return { createdAt: null, agentId: null, sessions: {} };
|
|
39
|
+
}
|
|
40
|
+
if (b.sessions && typeof b.sessions === 'object') return b; // already v1.1
|
|
41
|
+
const out = {
|
|
42
|
+
createdAt: b.createdAt || null,
|
|
43
|
+
agentId: b.agentId || null,
|
|
44
|
+
sessions: {},
|
|
45
|
+
};
|
|
46
|
+
if (b.sessionId) {
|
|
47
|
+
out.sessions.claude = {
|
|
48
|
+
sessionId: b.sessionId,
|
|
49
|
+
projectDir: b.projectDir,
|
|
50
|
+
jsonlPath: b.jsonlPath,
|
|
51
|
+
lastJsonlMtimeMs: b.lastJsonlMtimeMs || null,
|
|
52
|
+
lastDriveAt: b.lastDriveAt || null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
};
|
|
57
|
+
|
|
28
58
|
const loadState = () => {
|
|
29
59
|
let raw;
|
|
30
60
|
try {
|
|
@@ -35,6 +65,10 @@ const loadState = () => {
|
|
|
35
65
|
if (!raw.bindings || typeof raw.bindings !== 'object') {
|
|
36
66
|
raw.bindings = {};
|
|
37
67
|
}
|
|
68
|
+
// Migrate any v1 flat bindings to the per-agent shape.
|
|
69
|
+
for (const tid of Object.keys(raw.bindings)) {
|
|
70
|
+
raw.bindings[tid] = migrateBinding(raw.bindings[tid]);
|
|
71
|
+
}
|
|
38
72
|
return raw;
|
|
39
73
|
};
|
|
40
74
|
|
|
@@ -43,12 +77,33 @@ const saveState = (state) => {
|
|
|
43
77
|
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), 'utf8');
|
|
44
78
|
};
|
|
45
79
|
|
|
80
|
+
// The full per-thread binding ({ createdAt, agentId, sessions }), or null.
|
|
46
81
|
const getBinding = (state, threadId) =>
|
|
47
82
|
state.bindings && state.bindings[threadId] ? state.bindings[threadId] : null;
|
|
48
83
|
|
|
49
|
-
|
|
50
|
-
|
|
84
|
+
// One agent's session record on a thread, or null. The driver consumes this
|
|
85
|
+
// flat shape directly — same contract as the v1 binding.
|
|
86
|
+
const getAgentSession = (state, threadId, agent) => {
|
|
87
|
+
const b = getBinding(state, threadId);
|
|
88
|
+
if (!b || !b.sessions) return null;
|
|
89
|
+
return b.sessions[agent] || null;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Store/replace one agent's session on a thread. Creates the binding if absent.
|
|
93
|
+
const setAgentSession = (state, threadId, agent, session, meta) => {
|
|
94
|
+
let b = state.bindings[threadId];
|
|
95
|
+
if (!b || !b.sessions) {
|
|
96
|
+
b = {
|
|
97
|
+
createdAt: new Date().toISOString(),
|
|
98
|
+
agentId: (meta && meta.agentId) || null,
|
|
99
|
+
sessions: {},
|
|
100
|
+
};
|
|
101
|
+
state.bindings[threadId] = b;
|
|
102
|
+
}
|
|
103
|
+
if (meta && meta.agentId) b.agentId = meta.agentId;
|
|
104
|
+
b.sessions[agent] = session;
|
|
51
105
|
saveState(state);
|
|
106
|
+
return b;
|
|
52
107
|
};
|
|
53
108
|
|
|
54
109
|
module.exports = {
|
|
@@ -57,5 +112,6 @@ module.exports = {
|
|
|
57
112
|
loadState,
|
|
58
113
|
saveState,
|
|
59
114
|
getBinding,
|
|
60
|
-
|
|
115
|
+
getAgentSession,
|
|
116
|
+
setAgentSession,
|
|
61
117
|
};
|